3 Commits
Author SHA1 Message Date
dmg 3dfc1b71af fix(felling): support copper axes
Release / release (push) Successful in 2m17s
CI / build (push) Successful in 1m1s
2026-08-11 19:54:07 -04:00
dmg f26b7d0f0d fix(progress): count logs from disrupted trees
Release / release (push) Successful in 2m16s
CI / build (push) Successful in 59s
2026-08-11 18:35:04 -04:00
dmg e7a5a14529 fix(progress): count tree blocks broken with any tool
Release / release (push) Successful in 1m49s
CI / build (push) Successful in 58s
2026-08-11 18:27:21 -04:00
10 changed files with 78 additions and 14 deletions
+2 -2
View File
@@ -79,7 +79,7 @@ Administrative commands require `treefeller.admin`, granted to server operators
- Crimson and warped fungi
- Giant red and brown mushrooms
Azalea-grown logs count as oak. Bamboo is not treated as a tree.
Azalea-grown logs count as oak. Unlock progress counts unstripped natural log and Nether stem materials even after the surrounding tree has been disrupted; wood, hyphae, stripped variants, and bamboo do not count. Player-placed qualifying materials are indistinguishable from generated materials in Spigot and therefore also count. Giant mushroom stems require enough cap context to identify their species.
## Configuration and state
@@ -91,7 +91,7 @@ Player preferences, administrative locks, progress, unlocks, and known names are
Tree detection searches matching natural trunk blocks laterally and upward, never below the initiating chop, and requires corresponding foliage or caps. Search bounds prevent unbounded traversal. Leaves, caps, roots, vines, and decorations are not automatically broken.
Additional trunk blocks use Spigot's `Player.breakBlock` path so block-break cancellation, drops, experience, enchantments, and axe durability remain authoritative. Protection plugins should cancel `BlockBreakEvent` normally. A cancelled additional break stops the remaining felling.
Automatic felling supports Spigot 26.2 wooden, stone, copper, iron, golden, diamond, and netherite axes. Additional trunk blocks use Spigot's `Player.breakBlock` path so block-break cancellation, drops, experience, enchantments, and axe durability remain authoritative. Protection plugins should cancel `BlockBreakEvent` normally. A cancelled additional break stops the remaining felling.
## Releases
+21
View File
@@ -2,6 +2,27 @@
## 2026-08-11
### US-003 copper axe support corrected
- Reproduced copper axes failing automatic-felling eligibility because the explicit Spigot 26.2 axe-material set omitted `COPPER_AXE`.
- Added copper axes alongside wooden, stone, iron, golden, diamond, and netherite axes without broadening eligibility to pickaxes or other tools.
- Verified copper-axe classification, end-to-end felling activation, non-axe rejection, and the complete build with `./gradlew clean check jar`.
### US-001 disrupted-tree progress corrected
- Reproduced remaining lower logs failing to count after an upper trunk block was removed because progression unnecessarily re-ran full intact-tree validation.
- Progress now classifies unstripped natural `*_LOG`, crimson stem, and warped stem materials directly, so each qualifying block counts even after the tree structure is disrupted.
- Wood, hyphae, stripped variants, and bamboo remain excluded; player-placed qualifying materials count because Spigot does not expose generation provenance.
- Giant mushroom stems retain cap-based validation because their shared stem material cannot distinguish red from brown, while automatic felling still requires a validated intact tree and an axe.
- Verified disrupted-tree progress, excluded processed materials, tool-independent progress, axe-only felling, and the complete build with `./gradlew clean check jar`.
### US-001 tool-independent progress corrected
- Reproduced the progression gap caused by sharing the automatic-felling axe restriction with manual unlock progress.
- Qualifying validated Survival tree blocks now add progress when broken with an axe, another tool, or an empty hand; cancelled, Creative, and automatically felled blocks remain excluded.
- Automatic felling remains strictly axe-only, including for players who have already unlocked the species.
- Verified empty-hand and pickaxe progress, automatic-break suppression, axe-only felling, and the complete build with `./gradlew clean check jar`.
### US-010 offline operator identity corrected
- Reproduced WindMagi's missing operator privileges and found that name-based image setup stored the online account UUID while offline-mode login assigned deterministic UUID `6a3b6e9f-1a2f-380c-8a75-7a7ca6392c0e`.
@@ -13,8 +13,12 @@ As a **survival player**, I want to unlock Tree Feller by practicing with each t
- [x] Progress is tracked independently for oak, spruce, birch, jungle, acacia, dark oak, mangrove, cherry, pale oak, crimson fungi, warped fungi, giant red mushrooms, and giant brown mushrooms as represented by Spigot 26.2 materials.
- [x] Azalea-grown oak logs contribute to oak progress, and bamboo does not contribute to any tree species.
- [x] A block contributes progress only when a player manually breaks it in Survival mode, with an axe, from a structure that passes Tree Feller's tree validation.
- [x] Creative-mode breaks, non-axe breaks, cancelled breaks, and blocks removed by automatic felling do not contribute progress.
- [x] A manually broken unstripped natural trunk material with an unambiguous species contributes Survival progress even when the remaining structure no longer passes full tree validation.
- [x] Natural trunk materials include the supported `*_LOG`, `CRIMSON_STEM`, and `WARPED_STEM` blocks; wood, hyphae, stripped variants, and bamboo do not contribute.
- [x] Giant mushroom stems contribute only when surrounding cap context allows Tree Feller to distinguish red from brown.
- [x] Player-placed blocks using an otherwise qualifying natural trunk material contribute because Spigot does not expose reliable generation provenance.
- [x] A qualifying block contributes regardless of whether the player uses an axe, another tool, or an empty hand.
- [x] Creative-mode breaks, cancelled breaks, and blocks removed by automatic felling do not contribute progress.
- [x] Each qualifying manually mined block contributes exactly one point to its species.
- [x] Each species has an independently configurable unlock threshold that defaults to 100 blocks.
- [x] Reaching the active threshold permanently unlocks automatic felling for that species.
@@ -11,7 +11,7 @@ As a **player with an unlocked species**, I want its trees to break progressivel
## Acceptance criteria
- [x] Automatic felling is considered only for a non-cancelled Survival-mode block break made with an axe against a species the player has unlocked.
- [x] Automatic felling is considered only for a non-cancelled Survival-mode block break made with a supported wooden, stone, copper, iron, golden, diamond, or netherite axe against a species the player has unlocked.
- [x] Sneaking when the initiating block is broken always bypasses automatic felling and leaves the ordinary single-block break intact.
- [x] A disabled or administratively locked player receives the ordinary single-block break without automatic felling.
- [x] Tree detection follows connected blocks of the initiating trunk family laterally and upward, including diagonal branches, but never follows trunk blocks below the initiating block.
@@ -40,16 +40,19 @@ public final class TreeProgressListener implements Listener {
Player player = event.getPlayer();
if (event.isCancelled()
|| player.getGameMode() != GameMode.SURVIVAL
|| !TreeTools.isAxe(player.getInventory().getItemInMainHand().getType())
|| automaticBreaks.isMarked(event.getBlock())) {
return;
}
Optional<TreeStructure> detected = detector.detect(event.getBlock());
if (detected.isEmpty()) {
Optional<TreeSpecies> classified = TreeTaxonomy.directSpecies(event.getBlock().getType());
if (classified.isEmpty()
&& event.getBlock().getType() == org.bukkit.Material.MUSHROOM_STEM) {
classified = detector.detect(event.getBlock()).map(TreeStructure::species);
}
if (classified.isEmpty()) {
return;
}
TreeSpecies species = detected.orElseThrow().species();
TreeSpecies species = classified.orElseThrow();
PlayerTreeFellerState state = states.load(player.getUniqueId())
.orElseGet(() -> PlayerTreeFellerState.initial(
player.getUniqueId(), player.getName()));
@@ -9,6 +9,7 @@ public final class TreeTools {
private static final Set<Material> AXES = EnumSet.of(
Material.WOODEN_AXE,
Material.STONE_AXE,
Material.COPPER_AXE,
Material.IRON_AXE,
Material.GOLDEN_AXE,
Material.DIAMOND_AXE,
@@ -35,7 +35,7 @@ class TreeFellingListenerTest {
when(player.getName()).thenReturn("Player");
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
when(player.getInventory()).thenReturn(inventory);
when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.IRON_AXE));
when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.COPPER_AXE));
TreeStructure tree = new TreeStructure(
TreeSpecies.OAK, List.of(new BlockPoint(0, 0, 0), new BlockPoint(0, 1, 0)));
PlayerTreeFellerState eligible = PlayerTreeFellerState.initial(playerId, "Player")
@@ -54,6 +54,34 @@ class TreeFellingListenerTest {
verify(starter, org.mockito.Mockito.times(1)).start(any(), any(), any(), eq(2));
}
@Test
void aNonAxeCanEarnProgressButNeverStartsAutomaticFelling() {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
PlayerInventory inventory = mock(PlayerInventory.class);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Player");
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
when(player.getInventory()).thenReturn(inventory);
when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.DIAMOND_PICKAXE));
PlayerStateStore states = mock(PlayerStateStore.class);
when(states.load(playerId)).thenReturn(Optional.of(
PlayerTreeFellerState.initial(playerId, "Player")
.withUnlocked(TreeSpecies.OAK, true)));
TreeFellingStarter starter = mock(TreeFellingStarter.class);
TreeFellingListener listener = new TreeFellingListener(
ignored -> Optional.of(new TreeStructure(
TreeSpecies.OAK, List.of(new BlockPoint(0, 0, 0)))),
states,
new AutomaticBreakRegistry(),
starter,
() -> 2);
listener.onBlockBreak(new BlockBreakEvent(mock(Block.class), player));
verify(starter, never()).start(any(), any(), any(), any(Integer.class));
}
@Test
void administrativeLockSuppressesFellingWithoutChangingTheOrdinaryBreak() {
UUID playerId = UUID.randomUUID();
@@ -19,7 +19,7 @@ import org.junit.jupiter.api.Test;
class TreeProgressListenerTest {
@Test
void recordsOnlyManualSurvivalAxeBreaksFromValidatedTrees() throws Exception {
void recordsNaturalTrunkMaterialAfterTheTreeStructureIsNoLongerIntact() throws Exception {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
PlayerInventory inventory = mock(PlayerInventory.class);
@@ -31,15 +31,15 @@ class TreeProgressListenerTest {
when(player.getInventory()).thenReturn(inventory);
when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.IRON_AXE));
when(block.getWorld()).thenReturn(world);
when(block.getType()).thenReturn(Material.OAK_LOG);
when(world.getUID()).thenReturn(UUID.randomUUID());
when(block.getX()).thenReturn(1);
when(block.getY()).thenReturn(64);
when(block.getZ()).thenReturn(2);
TreeStructure tree = new TreeStructure(TreeSpecies.OAK, List.of(new BlockPoint(0, 0, 0)));
InMemoryStateStore states = new InMemoryStateStore();
AutomaticBreakRegistry automaticBreaks = new AutomaticBreakRegistry();
TreeProgressListener listener = new TreeProgressListener(
ignored -> Optional.of(tree),
ignored -> Optional.empty(),
states,
ignored -> 100,
automaticBreaks,
@@ -50,9 +50,12 @@ class TreeProgressListenerTest {
automaticBreaks.mark(block);
listener.onBlockBreak(event);
automaticBreaks.unmark(block);
when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.AIR));
listener.onBlockBreak(event);
when(inventory.getItemInMainHand()).thenReturn(new ItemStack(Material.DIAMOND_PICKAXE));
listener.onBlockBreak(event);
assertEquals(1, states.state.progress(TreeSpecies.OAK));
assertEquals(2, states.state.progress(TreeSpecies.OAK));
}
private static final class InMemoryStateStore implements PlayerStateStore {
@@ -26,5 +26,8 @@ class TreeTaxonomyTest {
expected.forEach((material, species) ->
assertEquals(species, TreeTaxonomy.directSpecies(material).orElseThrow()));
assertTrue(TreeTaxonomy.directSpecies(Material.BAMBOO).isEmpty());
assertTrue(TreeTaxonomy.directSpecies(Material.OAK_WOOD).isEmpty());
assertTrue(TreeTaxonomy.directSpecies(Material.STRIPPED_OAK_LOG).isEmpty());
assertTrue(TreeTaxonomy.directSpecies(Material.CRIMSON_HYPHAE).isEmpty());
}
}
@@ -11,6 +11,7 @@ class TreeToolsTest {
void acceptsAxesButNotPickaxesOrOtherItems() {
assertTrue(TreeTools.isAxe(Material.WOODEN_AXE));
assertTrue(TreeTools.isAxe(Material.GOLDEN_AXE));
assertTrue(TreeTools.isAxe(Material.COPPER_AXE));
assertTrue(TreeTools.isAxe(Material.NETHERITE_AXE));
assertFalse(TreeTools.isAxe(Material.DIAMOND_PICKAXE));
assertFalse(TreeTools.isAxe(Material.AIR));