mirror of
https://github.com/mcMMO-Dev/mcMMO.git
synced 2026-08-18 11:26:06 +00:00
Fix KnockOnWood XP orbs never spawning on nether tree cap blocks during Tree Feller
Nether_Wart_Block and Warped_Wart_Block both have woodcutting XP in experience.yml AND are in the treeFellerDestructibleWhiteList. The drop routing in dropTreeFellerLootFromBlocks used an if/else-if structure, so blocks with woodcutting XP always took the first branch and never reached the else-if where the KnockOnWood orb logic lived. Fix: extract the orb spawning block into a standalone if-check that runs after the drop routing, so it fires for any isNonWoodPartOfTree block regardless of whether it also has woodcutting XP. Fixes #5288
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
Version 2.2.053
|
||||
Fixed KnockOnWood XP orbs never spawning on nether/warped tree cap blocks (nether_wart_block, warped_wart_block) during Tree Feller — those blocks grant woodcutting XP and are also tree-feller destructible, but the orb logic was unreachable for them due to an if/else-if routing bug
|
||||
(Codebase) Removed 27 dead JSON.* locale keys from all locale files — JSON.Rank, JSON.JWrapper.Header, JSON.JWrapper.Target.{Type,Block,Player}, JSON.Hover.{SuperAbility,Mystery2}, JSON.Notification.SuperAbility, JSON.Acrobatics.Roll.Interaction.Activated, and all JSON.<SkillName> skill-name keys were never loaded in Java source; skill names use <Skill>.SkillName instead
|
||||
|
||||
Version 2.2.052
|
||||
|
||||
@ -400,17 +400,20 @@ public class WoodcuttingManager extends SkillManager {
|
||||
player
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Drop displaced non-woodcutting XP blocks
|
||||
if (hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
|
||||
if (RankUtils.hasReachedRank(2, player,
|
||||
SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
|
||||
if (mcMMO.p.getAdvancedConfig().isKnockOnWoodXPOrbEnabled()) {
|
||||
if (ProbabilityUtil.isStaticSkillRNGSuccessful(
|
||||
PrimarySkillType.WOODCUTTING, mmoPlayer, 10)) {
|
||||
int randOrbCount = Math.max(1, Misc.getRandom().nextInt(100));
|
||||
Misc.spawnExperienceOrb(block.getLocation(), randOrbCount);
|
||||
}
|
||||
// KnockOnWood XP orbs apply to any non-log tree component, including blocks that
|
||||
// also grant woodcutting XP (e.g. nether/warped wart blocks). Previously this was
|
||||
// nested inside the else-if above, which prevented orbs from spawning on nether tree
|
||||
// caps because they have woodcutting XP and never reached the else-if branch.
|
||||
if (BlockUtils.isNonWoodPartOfTree(block)
|
||||
&& hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
|
||||
if (RankUtils.hasReachedRank(2, player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) {
|
||||
if (mcMMO.p.getAdvancedConfig().isKnockOnWoodXPOrbEnabled()) {
|
||||
if (ProbabilityUtil.isStaticSkillRNGSuccessful(
|
||||
PrimarySkillType.WOODCUTTING, mmoPlayer, 10)) {
|
||||
int randOrbCount = Math.max(1, Misc.getRandom().nextInt(100));
|
||||
Misc.spawnExperienceOrb(block.getLocation(), randOrbCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,21 +17,28 @@ import static org.mockito.Mockito.mockStatic;
|
||||
import com.gmail.nossr50.MMOTestEnvironment;
|
||||
import com.gmail.nossr50.api.exceptions.InvalidSkillException;
|
||||
import com.gmail.nossr50.config.experience.ExperienceConfig;
|
||||
import com.gmail.nossr50.datatypes.player.McMMOPlayer;
|
||||
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
|
||||
import com.gmail.nossr50.datatypes.skills.SubSkillType;
|
||||
import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
|
||||
import com.gmail.nossr50.mcMMO;
|
||||
import com.gmail.nossr50.util.BlockUtils;
|
||||
import com.gmail.nossr50.util.EventUtils;
|
||||
import com.gmail.nossr50.util.MetadataConstants;
|
||||
import com.gmail.nossr50.util.Misc;
|
||||
import com.gmail.nossr50.util.random.ProbabilityUtil;
|
||||
import com.gmail.nossr50.util.skills.RankUtils;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
@ -43,6 +50,7 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
|
||||
class WoodcuttingTest extends MMOTestEnvironment {
|
||||
private static final Logger logger = getLogger(WoodcuttingTest.class.getName());
|
||||
@ -317,4 +325,159 @@ class WoodcuttingTest extends MMOTestEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class KnockOnWoodXpOrbSpawning {
|
||||
// Covers the bug where KnockOnWood XP orbs never spawned for nether tree cap
|
||||
// blocks (nether_wart_block, warped_wart_block). Those blocks appear in both
|
||||
// experience.yml (hasWoodcuttingXP=true) and the treeFellerDestructibleWhiteList
|
||||
// (isNonWoodPartOfTree=true). The old code placed the XP orb logic inside the
|
||||
// else-if branch for isNonWoodPartOfTree, so it was unreachable for any block
|
||||
// that also had woodcutting XP. The fix extracts the orb logic into a standalone
|
||||
// if-block that runs after the if/else-if drop routing.
|
||||
|
||||
@Test
|
||||
void netherWartBlock_spawnXpOrb_whenKnockOnWoodRank2Unlocked() {
|
||||
// Given: player has KnockOnWood at rank 2 and XP orbs are enabled
|
||||
mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000);
|
||||
Mockito.when(RankUtils.hasUnlockedSubskill(any(Player.class),
|
||||
eq(SubSkillType.WOODCUTTING_KNOCK_ON_WOOD))).thenReturn(true);
|
||||
Mockito.when(RankUtils.hasReachedRank(eq(2), any(Player.class),
|
||||
eq(SubSkillType.WOODCUTTING_KNOCK_ON_WOOD))).thenReturn(true);
|
||||
Mockito.when(advancedConfig.isKnockOnWoodXPOrbEnabled()).thenReturn(true);
|
||||
|
||||
// Given: block that satisfies BOTH hasWoodcuttingXP AND isNonWoodPartOfTree
|
||||
// (matches nether_wart_block / warped_wart_block — the previously broken case)
|
||||
final Block netherWartBlock = mock(Block.class, "netherWartBlock");
|
||||
final Location blockLocation = new Location(world, 10, 64, 10);
|
||||
Mockito.when(netherWartBlock.getDrops(any())).thenReturn(Collections.emptyList());
|
||||
Mockito.when(netherWartBlock.getLocation()).thenReturn(blockLocation);
|
||||
|
||||
// Stub processBonusDropCheck so it does not exercise unrelated paths
|
||||
Mockito.doNothing().when(woodcuttingManager).processBonusDropCheck(any(Block.class));
|
||||
|
||||
try (MockedStatic<BlockUtils> mockedBlockUtils = mockStatic(BlockUtils.class);
|
||||
MockedStatic<EventUtils> localMockedEventUtils = mockStatic(EventUtils.class);
|
||||
MockedStatic<ProbabilityUtil> mockedProbabilityUtil =
|
||||
mockStatic(ProbabilityUtil.class)) {
|
||||
|
||||
mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class)))
|
||||
.thenReturn(true);
|
||||
mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class)))
|
||||
.thenReturn(true);
|
||||
localMockedEventUtils.when(() -> EventUtils.simulateBlockBreak(
|
||||
any(Block.class), any(Player.class), any())).thenReturn(true);
|
||||
// Force the 10% RNG check to always succeed so the orb always spawns
|
||||
mockedProbabilityUtil.when(() -> ProbabilityUtil.isStaticSkillRNGSuccessful(
|
||||
any(PrimarySkillType.class), any(McMMOPlayer.class), anyDouble()))
|
||||
.thenReturn(true);
|
||||
|
||||
// Wire Misc.getRandom() to a predictable stub so the orb count is deterministic
|
||||
final Random stubRandom = mock(Random.class);
|
||||
Mockito.when(Misc.getRandom()).thenReturn(stubRandom);
|
||||
Mockito.when(stubRandom.nextInt(anyInt())).thenReturn(50);
|
||||
|
||||
// When: Tree Feller loot is processed for this block
|
||||
invokeDropTreeFellerLootFromBlocks(Set.of(netherWartBlock));
|
||||
|
||||
// Then: an XP orb was spawned — this was the bug; this call was unreachable
|
||||
// before the fix because the orb code was inside the else-if for isNonWoodPartOfTree
|
||||
mockedMisc.verify(() -> Misc.spawnExperienceOrb(eq(blockLocation), anyInt()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void regularLeaf_spawnXpOrb_whenKnockOnWoodRank2Unlocked() {
|
||||
// Given: a regular leaf block — only isNonWoodPartOfTree=true, no woodcutting XP.
|
||||
// Regression check: existing leaf behaviour continues to work after the fix.
|
||||
mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000);
|
||||
Mockito.when(RankUtils.hasUnlockedSubskill(any(Player.class),
|
||||
eq(SubSkillType.WOODCUTTING_KNOCK_ON_WOOD))).thenReturn(true);
|
||||
Mockito.when(RankUtils.hasReachedRank(eq(2), any(Player.class),
|
||||
eq(SubSkillType.WOODCUTTING_KNOCK_ON_WOOD))).thenReturn(true);
|
||||
Mockito.when(advancedConfig.isKnockOnWoodXPOrbEnabled()).thenReturn(true);
|
||||
|
||||
final Block leafBlock = mock(Block.class, "leafBlock");
|
||||
final Location blockLocation = new Location(world, 10, 64, 10);
|
||||
Mockito.when(leafBlock.getDrops(any())).thenReturn(Collections.emptyList());
|
||||
Mockito.when(leafBlock.getLocation()).thenReturn(blockLocation);
|
||||
|
||||
try (MockedStatic<BlockUtils> mockedBlockUtils = mockStatic(BlockUtils.class);
|
||||
MockedStatic<EventUtils> localMockedEventUtils = mockStatic(EventUtils.class);
|
||||
MockedStatic<ProbabilityUtil> mockedProbabilityUtil =
|
||||
mockStatic(ProbabilityUtil.class)) {
|
||||
|
||||
mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class)))
|
||||
.thenReturn(false);
|
||||
mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class)))
|
||||
.thenReturn(true);
|
||||
localMockedEventUtils.when(() -> EventUtils.simulateBlockBreak(
|
||||
any(Block.class), any(Player.class), any())).thenReturn(true);
|
||||
mockedProbabilityUtil.when(() -> ProbabilityUtil.isStaticSkillRNGSuccessful(
|
||||
any(PrimarySkillType.class), any(McMMOPlayer.class), anyDouble()))
|
||||
.thenReturn(true);
|
||||
|
||||
final Random stubRandom = mock(Random.class);
|
||||
Mockito.when(Misc.getRandom()).thenReturn(stubRandom);
|
||||
Mockito.when(stubRandom.nextInt(anyInt())).thenReturn(50);
|
||||
|
||||
// When
|
||||
invokeDropTreeFellerLootFromBlocks(Set.of(leafBlock));
|
||||
|
||||
// Then: orb still spawns for leaves (unchanged behaviour)
|
||||
mockedMisc.verify(() -> Misc.spawnExperienceOrb(eq(blockLocation), anyInt()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void regularLog_doesNotSpawnXpOrb() {
|
||||
// Given: a regular log block (hasWoodcuttingXP=true, isNonWoodPartOfTree=false).
|
||||
// XP orbs must NEVER fire for plain logs — only for non-log tree components.
|
||||
mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000);
|
||||
Mockito.when(RankUtils.hasUnlockedSubskill(any(Player.class),
|
||||
eq(SubSkillType.WOODCUTTING_KNOCK_ON_WOOD))).thenReturn(true);
|
||||
Mockito.when(RankUtils.hasReachedRank(eq(2), any(Player.class),
|
||||
eq(SubSkillType.WOODCUTTING_KNOCK_ON_WOOD))).thenReturn(true);
|
||||
Mockito.when(advancedConfig.isKnockOnWoodXPOrbEnabled()).thenReturn(true);
|
||||
|
||||
final Block logBlock = mock(Block.class, "logBlock");
|
||||
Mockito.when(logBlock.getDrops(any())).thenReturn(Collections.emptyList());
|
||||
|
||||
// Stub processBonusDropCheck so it does not exercise unrelated paths
|
||||
Mockito.doNothing().when(woodcuttingManager).processBonusDropCheck(any(Block.class));
|
||||
|
||||
try (MockedStatic<BlockUtils> mockedBlockUtils = mockStatic(BlockUtils.class);
|
||||
MockedStatic<EventUtils> localMockedEventUtils = mockStatic(EventUtils.class);
|
||||
MockedStatic<ProbabilityUtil> mockedProbabilityUtil =
|
||||
mockStatic(ProbabilityUtil.class)) {
|
||||
|
||||
mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class)))
|
||||
.thenReturn(true);
|
||||
mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class)))
|
||||
.thenReturn(false);
|
||||
localMockedEventUtils.when(() -> EventUtils.simulateBlockBreak(
|
||||
any(Block.class), any(Player.class), any())).thenReturn(true);
|
||||
mockedProbabilityUtil.when(() -> ProbabilityUtil.isStaticSkillRNGSuccessful(
|
||||
any(PrimarySkillType.class), any(McMMOPlayer.class), anyDouble()))
|
||||
.thenReturn(true);
|
||||
|
||||
// When
|
||||
invokeDropTreeFellerLootFromBlocks(Set.of(logBlock));
|
||||
|
||||
// Then: no XP orb spawned for a plain log
|
||||
mockedMisc.verify(() -> Misc.spawnExperienceOrb(any(), anyInt()), never());
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeDropTreeFellerLootFromBlocks(final Set<Block> blocks) {
|
||||
try {
|
||||
final Method method = WoodcuttingManager.class.getDeclaredMethod(
|
||||
"dropTreeFellerLootFromBlocks", Set.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(woodcuttingManager, blocks);
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user