Compare commits

...

3 Commits

Author SHA1 Message Date
nossr50
b91fa2cf37 back to dev 2025-07-08 13:08:08 -07:00
nossr50
6acc4b1ec6 2.2.040 2025-07-06 12:27:25 -07:00
nossr50
41b5667cd4 Some more unit test coverage for tree feller 2025-07-04 13:27:38 -07:00
8 changed files with 188 additions and 88 deletions

View File

@ -1,7 +1,8 @@
Version 2.2.040 Version 2.2.040
Fixed hover component and action bar messages not working for 1.21.6 and 1.21.7
Fixed bug where entries of mctop could be duplicated when using FlatFile
Fixed bug where a party leader could leave a party and the party would be left without a party leader Fixed bug where a party leader could leave a party and the party would be left without a party leader
Fixed a bug where EcoEnchants or other similar plugins could cause an infinite loop within mcMMO Fixed a bug where EcoEnchants and similar plugins could cause an infinite loop within mcMMO during TreeFeller and other abilities
(Codebase) Updated Adventure Libs
Added 'Happy_Ghast' to experience.yml for combat XP Added 'Happy_Ghast' to experience.yml for combat XP
Added 'Ghastling' to experience.yml for combat XP Added 'Ghastling' to experience.yml for combat XP
Updated Japanese (ja_JP) translation (Thanks ryota-abe) Updated Japanese (ja_JP) translation (Thanks ryota-abe)

View File

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.nossr50.mcMMO</groupId> <groupId>com.gmail.nossr50.mcMMO</groupId>
<artifactId>mcMMO</artifactId> <artifactId>mcMMO</artifactId>
<version>2.2.040-SNAPSHOT</version> <version>2.2.041-SNAPSHOT</version>
<name>mcMMO</name> <name>mcMMO</name>
<url>https://github.com/mcMMO-Dev/mcMMO</url> <url>https://github.com/mcMMO-Dev/mcMMO</url>
<scm> <scm>

View File

@ -21,11 +21,11 @@ import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeSet;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
@ -36,9 +36,9 @@ import org.jetbrains.annotations.Nullable;
public final class FlatFileDatabaseManager implements DatabaseManager { public final class FlatFileDatabaseManager implements DatabaseManager {
public static final String IGNORED = "IGNORED"; public static final String IGNORED = "IGNORED";
public static final String LEGACY_INVALID_OLD_USERNAME = "_INVALID_OLD_USERNAME_'"; public static final String LEGACY_INVALID_OLD_USERNAME = "_INVALID_OLD_USERNAME_'";
private final @NotNull EnumMap<PrimarySkillType, List<PlayerStat>> playerStatHash = new EnumMap<>( private final @NotNull EnumMap<PrimarySkillType, List<PlayerStat>> leaderboardMap = new EnumMap<>(
PrimarySkillType.class); PrimarySkillType.class);
private final @NotNull List<PlayerStat> powerLevels = new ArrayList<>(); private @NotNull List<PlayerStat> powerLevels = new ArrayList<>();
private long lastUpdate = 0; private long lastUpdate = 0;
private final @NotNull String usersFilePath; private final @NotNull String usersFilePath;
private final @NotNull Logger logger; private final @NotNull Logger logger;
@ -562,7 +562,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
updateLeaderboards(); updateLeaderboards();
List<PlayerStat> statsList = List<PlayerStat> statsList =
primarySkillType == null ? powerLevels : playerStatHash.get(primarySkillType); primarySkillType == null ? powerLevels : leaderboardMap.get(primarySkillType);
int fromIndex = (Math.max(pageNumber, 1) - 1) * statsPerPage; int fromIndex = (Math.max(pageNumber, 1) - 1) * statsPerPage;
return statsList.subList(Math.min(fromIndex, statsList.size()), return statsList.subList(Math.min(fromIndex, statsList.size()),
@ -575,10 +575,9 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
HashMap<PrimarySkillType, Integer> skills = new HashMap<>(); HashMap<PrimarySkillType, Integer> skills = new HashMap<>();
for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) { for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) {
skills.put(skill, getPlayerRank(playerName, playerStatHash.get(skill))); skills.put(skill, getPlayerRank(playerName, leaderboardMap.get(skill)));
} }
//TODO: Gross
skills.put(null, getPlayerRank(playerName, powerLevels)); skills.put(null, getPlayerRank(playerName, powerLevels));
return skills; return skills;
@ -1039,25 +1038,24 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
} }
lastUpdate = System.currentTimeMillis(); // Log when the last update was run lastUpdate = System.currentTimeMillis(); // Log when the last update was run
powerLevels.clear(); // Clear old values from the power levels
// Initialize lists TreeSet<PlayerStat> powerLevelStats = new TreeSet<>();
List<PlayerStat> mining = new ArrayList<>(); TreeSet<PlayerStat> mining = new TreeSet<>();
List<PlayerStat> woodcutting = new ArrayList<>(); TreeSet<PlayerStat> woodcutting = new TreeSet<>();
List<PlayerStat> herbalism = new ArrayList<>(); TreeSet<PlayerStat> herbalism = new TreeSet<>();
List<PlayerStat> excavation = new ArrayList<>(); TreeSet<PlayerStat> excavation = new TreeSet<>();
List<PlayerStat> acrobatics = new ArrayList<>(); TreeSet<PlayerStat> acrobatics = new TreeSet<>();
List<PlayerStat> repair = new ArrayList<>(); TreeSet<PlayerStat> repair = new TreeSet<>();
List<PlayerStat> swords = new ArrayList<>(); TreeSet<PlayerStat> swords = new TreeSet<>();
List<PlayerStat> axes = new ArrayList<>(); TreeSet<PlayerStat> axes = new TreeSet<>();
List<PlayerStat> archery = new ArrayList<>(); TreeSet<PlayerStat> archery = new TreeSet<>();
List<PlayerStat> unarmed = new ArrayList<>(); TreeSet<PlayerStat> unarmed = new TreeSet<>();
List<PlayerStat> taming = new ArrayList<>(); TreeSet<PlayerStat> taming = new TreeSet<>();
List<PlayerStat> fishing = new ArrayList<>(); TreeSet<PlayerStat> fishing = new TreeSet<>();
List<PlayerStat> alchemy = new ArrayList<>(); TreeSet<PlayerStat> alchemy = new TreeSet<>();
List<PlayerStat> crossbows = new ArrayList<>(); TreeSet<PlayerStat> crossbows = new TreeSet<>();
List<PlayerStat> tridents = new ArrayList<>(); TreeSet<PlayerStat> tridents = new TreeSet<>();
List<PlayerStat> maces = new ArrayList<>(); TreeSet<PlayerStat> maces = new TreeSet<>();
BufferedReader in = null; BufferedReader in = null;
String playerName = null; String playerName = null;
@ -1106,7 +1104,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
skills.get(PrimarySkillType.TRIDENTS)); skills.get(PrimarySkillType.TRIDENTS));
powerLevel += putStat(maces, playerName, skills.get(PrimarySkillType.MACES)); powerLevel += putStat(maces, playerName, skills.get(PrimarySkillType.MACES));
putStat(powerLevels, playerName, powerLevel); putStat(powerLevelStats, playerName, powerLevel);
} }
} catch (Exception e) { } catch (Exception e) {
logger.severe( logger.severe(
@ -1125,42 +1123,23 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
} }
SkillComparator c = new SkillComparator(); powerLevels = List.copyOf(powerLevelStats);
leaderboardMap.put(PrimarySkillType.MINING, List.copyOf(mining));
mining.sort(c); leaderboardMap.put(PrimarySkillType.WOODCUTTING, List.copyOf(woodcutting));
woodcutting.sort(c); leaderboardMap.put(PrimarySkillType.REPAIR, List.copyOf(repair));
repair.sort(c); leaderboardMap.put(PrimarySkillType.UNARMED, List.copyOf(unarmed));
unarmed.sort(c); leaderboardMap.put(PrimarySkillType.HERBALISM, List.copyOf(herbalism));
herbalism.sort(c); leaderboardMap.put(PrimarySkillType.EXCAVATION, List.copyOf(excavation));
excavation.sort(c); leaderboardMap.put(PrimarySkillType.ARCHERY, List.copyOf(archery));
archery.sort(c); leaderboardMap.put(PrimarySkillType.SWORDS, List.copyOf(swords));
swords.sort(c); leaderboardMap.put(PrimarySkillType.AXES, List.copyOf(axes));
axes.sort(c); leaderboardMap.put(PrimarySkillType.ACROBATICS, List.copyOf(acrobatics));
acrobatics.sort(c); leaderboardMap.put(PrimarySkillType.TAMING, List.copyOf(taming));
taming.sort(c); leaderboardMap.put(PrimarySkillType.FISHING, List.copyOf(fishing));
fishing.sort(c); leaderboardMap.put(PrimarySkillType.ALCHEMY, List.copyOf(alchemy));
alchemy.sort(c); leaderboardMap.put(PrimarySkillType.CROSSBOWS, List.copyOf(crossbows));
crossbows.sort(c); leaderboardMap.put(PrimarySkillType.TRIDENTS, List.copyOf(tridents));
tridents.sort(c); leaderboardMap.put(PrimarySkillType.MACES, List.copyOf(maces));
maces.sort(c);
powerLevels.sort(c);
playerStatHash.put(PrimarySkillType.MINING, mining);
playerStatHash.put(PrimarySkillType.WOODCUTTING, woodcutting);
playerStatHash.put(PrimarySkillType.REPAIR, repair);
playerStatHash.put(PrimarySkillType.UNARMED, unarmed);
playerStatHash.put(PrimarySkillType.HERBALISM, herbalism);
playerStatHash.put(PrimarySkillType.EXCAVATION, excavation);
playerStatHash.put(PrimarySkillType.ARCHERY, archery);
playerStatHash.put(PrimarySkillType.SWORDS, swords);
playerStatHash.put(PrimarySkillType.AXES, axes);
playerStatHash.put(PrimarySkillType.ACROBATICS, acrobatics);
playerStatHash.put(PrimarySkillType.TAMING, taming);
playerStatHash.put(PrimarySkillType.FISHING, fishing);
playerStatHash.put(PrimarySkillType.ALCHEMY, alchemy);
playerStatHash.put(PrimarySkillType.CROSSBOWS, crossbows);
playerStatHash.put(PrimarySkillType.TRIDENTS, tridents);
playerStatHash.put(PrimarySkillType.MACES, maces);
return LeaderboardStatus.UPDATED; return LeaderboardStatus.UPDATED;
} }
@ -1280,7 +1259,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
int currentPos = 1; int currentPos = 1;
for (PlayerStat stat : statsList) { for (PlayerStat stat : statsList) {
if (stat.name.equalsIgnoreCase(playerName)) { if (stat.playerName().equalsIgnoreCase(playerName)) {
return currentPos; return currentPos;
} }
@ -1290,18 +1269,11 @@ public final class FlatFileDatabaseManager implements DatabaseManager {
return null; return null;
} }
private int putStat(List<PlayerStat> statList, String playerName, int statValue) { private int putStat(TreeSet<PlayerStat> statList, String playerName, int statValue) {
statList.add(new PlayerStat(playerName, statValue)); statList.add(new PlayerStat(playerName, statValue));
return statValue; return statValue;
} }
private static class SkillComparator implements Comparator<PlayerStat> {
@Override
public int compare(PlayerStat o1, PlayerStat o2) {
return (o2.statVal - o1.statVal);
}
}
private PlayerProfile loadFromLine(@NotNull String[] character) { private PlayerProfile loadFromLine(@NotNull String[] character) {
Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(character); // Skill levels Map<PrimarySkillType, Integer> skills = getSkillMapFromLine(character); // Skill levels
Map<PrimarySkillType, Float> skillsXp = new EnumMap<>( Map<PrimarySkillType, Float> skillsXp = new EnumMap<>(

View File

@ -1,11 +1,14 @@
package com.gmail.nossr50.datatypes.database; package com.gmail.nossr50.datatypes.database;
public class PlayerStat { import org.jetbrains.annotations.NotNull;
public String name;
public int statVal = 0;
public PlayerStat(String name, int value) { public record PlayerStat(String playerName, int value) implements Comparable<PlayerStat> {
this.name = name; @Override
this.statVal = value; public int compareTo(@NotNull PlayerStat o) {
// Descending order
int cmp = Integer.compare(o.value, this.value);
if (cmp != 0) return cmp;
// Tie-breaker
return this.playerName.compareTo(o.playerName);
} }
} }

View File

@ -79,10 +79,11 @@ public class MctopCommandDisplayTask extends CancellableRunnable {
// 12. Playername - skill value // 12. Playername - skill value
if (sender instanceof Player) { if (sender instanceof Player) {
sender.sendMessage( sender.sendMessage(
String.format("%2d. %s%s - %s%s", place, ChatColor.GREEN, stat.name, String.format("%2d. %s%s - %s%s", place, ChatColor.GREEN, stat.playerName(),
ChatColor.WHITE, stat.statVal)); ChatColor.WHITE, stat.value()));
} else { } else {
sender.sendMessage(String.format("%2d. %s - %s", place, stat.name, stat.statVal)); sender.sendMessage(String.format("%2d. %s - %s", place, stat.playerName(),
stat.value()));
} }
place++; place++;

View File

@ -43,6 +43,7 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
public class WoodcuttingManager extends SkillManager { public class WoodcuttingManager extends SkillManager {
public static final String SAPLING = "sapling"; public static final String SAPLING = "sapling";
@ -206,7 +207,8 @@ public class WoodcuttingManager extends SkillManager {
* and 10-15 milliseconds on jungle trees once the JIT has optimized the function (use the * and 10-15 milliseconds on jungle trees once the JIT has optimized the function (use the
* ability about 4 times before taking measurements). * ability about 4 times before taking measurements).
*/ */
private void processTree(Block block, Set<Block> treeFellerBlocks) { @VisibleForTesting
void processTree(Block block, Set<Block> treeFellerBlocks) {
List<Block> futureCenterBlocks = new ArrayList<>(); List<Block> futureCenterBlocks = new ArrayList<>();
// Check the block up and take different behavior (smaller search) if it's a log // Check the block up and take different behavior (smaller search) if it's a log

View File

@ -655,13 +655,13 @@ public class ScoreboardWrapper {
public void acceptLeaderboardData(@NotNull List<PlayerStat> leaderboardData) { public void acceptLeaderboardData(@NotNull List<PlayerStat> leaderboardData) {
for (PlayerStat stat : leaderboardData) { for (PlayerStat stat : leaderboardData) {
String name = stat.name; String name = stat.playerName();
if (name.equals(playerName)) { if (name.equals(playerName)) {
name = ChatColor.GOLD + "--You--"; name = ChatColor.GOLD + "--You--";
} }
sidebarObjective.getScore(name).setScore(stat.statVal); sidebarObjective.getScore(name).setScore(stat.value());
} }
} }

View File

@ -1,24 +1,40 @@
package com.gmail.nossr50.skills.woodcutting; package com.gmail.nossr50.skills.woodcutting;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import com.gmail.nossr50.MMOTestEnvironment; import com.gmail.nossr50.MMOTestEnvironment;
import com.gmail.nossr50.api.exceptions.InvalidSkillException; import com.gmail.nossr50.api.exceptions.InvalidSkillException;
import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.config.experience.ExperienceConfig;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.BlockUtils;
import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.skills.RankUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
class WoodcuttingTest extends MMOTestEnvironment { class WoodcuttingTest extends MMOTestEnvironment {
@ -69,7 +85,7 @@ class WoodcuttingTest extends MMOTestEnvironment {
void harvestLumberShouldDoubleDrop() { void harvestLumberShouldDoubleDrop() {
mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000); mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000);
Block block = Mockito.mock(Block.class); Block block = mock(Block.class);
// return empty collection if ItemStack // return empty collection if ItemStack
Mockito.when(block.getDrops(any())).thenReturn(Collections.emptyList()); Mockito.when(block.getDrops(any())).thenReturn(Collections.emptyList());
Mockito.when(block.getType()).thenReturn(Material.OAK_LOG); Mockito.when(block.getType()).thenReturn(Material.OAK_LOG);
@ -86,7 +102,7 @@ class WoodcuttingTest extends MMOTestEnvironment {
void harvestLumberShouldNotDoubleDrop() { void harvestLumberShouldNotDoubleDrop() {
mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 0); mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 0);
Block block = Mockito.mock(Block.class); Block block = mock(Block.class);
// wire block // wire block
Mockito.when(block.getDrops(any())).thenReturn(null); Mockito.when(block.getDrops(any())).thenReturn(null);
@ -99,7 +115,7 @@ class WoodcuttingTest extends MMOTestEnvironment {
@Test @Test
void testProcessWoodcuttingBlockXP() { void testProcessWoodcuttingBlockXP() {
Block targetBlock = Mockito.mock(Block.class); Block targetBlock = mock(Block.class);
Mockito.when(targetBlock.getType()).thenReturn(Material.OAK_LOG); Mockito.when(targetBlock.getType()).thenReturn(Material.OAK_LOG);
// wire XP // wire XP
Mockito.when(ExperienceConfig.getInstance() Mockito.when(ExperienceConfig.getInstance()
@ -110,4 +126,109 @@ class WoodcuttingTest extends MMOTestEnvironment {
Mockito.verify(mmoPlayer, Mockito.times(1)) Mockito.verify(mmoPlayer, Mockito.times(1))
.beginXpGain(eq(PrimarySkillType.WOODCUTTING), eq(5F), any(), any()); .beginXpGain(eq(PrimarySkillType.WOODCUTTING), eq(5F), any(), any());
} }
@Test
void treeFellerShouldStopAtThreshold() {
// Set threshold artificially low
int fakeThreshold = 3;
Mockito.when(generalConfig.getTreeFellerThreshold()).thenReturn(fakeThreshold);
WoodcuttingManager manager = Mockito.spy(new WoodcuttingManager(mmoPlayer));
// Simulate all blocks are logs with XP
MockedStatic<BlockUtils> mockedBlockUtils = mockStatic(BlockUtils.class);
mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class))).thenReturn(true);
mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class)))
.thenReturn(false);
// Simulate that block tracker always allows processing
Mockito.when(mcMMO.getUserBlockTracker().isIneligible(any(Block.class))).thenReturn(false);
// Create distinct mocked blocks to simulate recursion
Block centerBlock = mock(Block.class);
List<Block> relatives = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Block relative = mock(Block.class, "block_" + i);
Mockito.when(relative.getRelative(any(BlockFace.class))).thenReturn(relative);
Mockito.when(relative.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(relative);
relatives.add(relative);
}
// Wire center block to return a different relative each time
Mockito.when(centerBlock.getRelative(any(BlockFace.class)))
.thenAnswer(inv -> relatives.get(0));
Mockito.when(centerBlock.getRelative(anyInt(), anyInt(), anyInt()))
.thenAnswer(inv -> relatives.get(
ThreadLocalRandom.current().nextInt(relatives.size())));
Set<Block> treeFellerBlocks = new HashSet<>();
manager.processTree(centerBlock, treeFellerBlocks);
// --- Assertions ---
// It processed *at least one* block
assertFalse(treeFellerBlocks.isEmpty(), "Tree Feller should process at least one block");
// It reached or slightly exceeded the threshold
assertTrue(treeFellerBlocks.size() >= fakeThreshold,
"Tree Feller should process up to the threshold limit");
// Confirm it stopped due to the threshold
assertTrue(getPrivateTreeFellerReachedThreshold(manager),
"Tree Feller should set treeFellerReachedThreshold to true");
mockedBlockUtils.close();
}
private boolean getPrivateTreeFellerReachedThreshold(WoodcuttingManager manager) {
try {
Field field = WoodcuttingManager.class.getDeclaredField("treeFellerReachedThreshold");
field.setAccessible(true);
return (boolean) field.get(manager);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
void treeFellerShouldNotReachThreshold() throws NoSuchFieldException, IllegalAccessException {
int threshold = 10;
Mockito.when(generalConfig.getTreeFellerThreshold()).thenReturn(threshold);
WoodcuttingManager manager = Mockito.spy(new WoodcuttingManager(mmoPlayer));
MockedStatic<BlockUtils> mockedBlockUtils = mockStatic(BlockUtils.class);
mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class))).thenReturn(true);
mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class)))
.thenReturn(false);
Mockito.when(mcMMO.getUserBlockTracker().isIneligible(any(Block.class))).thenReturn(false);
// Create 4 blocks (well below threshold)
Block b0 = mock(Block.class, "b0");
Block b1 = mock(Block.class, "b1");
Block b2 = mock(Block.class, "b2");
Block b3 = mock(Block.class, "b3");
// Deterministically chain recursion: b0 b1 b2 b3 null
Mockito.when(b0.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(b1);
Mockito.when(b1.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(b2);
Mockito.when(b2.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(b3);
Mockito.when(b3.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(null);
Mockito.when(b0.getRelative(any(BlockFace.class))).thenReturn(b1);
Mockito.when(b1.getRelative(any(BlockFace.class))).thenReturn(b2);
Mockito.when(b2.getRelative(any(BlockFace.class))).thenReturn(b3);
Mockito.when(b3.getRelative(any(BlockFace.class))).thenReturn(null);
Set<Block> processed = new HashSet<>();
manager.processTree(b0, processed);
assertEquals(3, processed.size(), "Should process exactly 4 blocks");
assertFalse(getPrivateTreeFellerReachedThreshold(manager),
"treeFellerReachedThreshold should remain false");
mockedBlockUtils.close();
}
} }