diff --git a/src/main/java/baritone/pathing/movement/MovementHelper.java b/src/main/java/baritone/pathing/movement/MovementHelper.java index 19eeaee42..8b513f143 100644 --- a/src/main/java/baritone/pathing/movement/MovementHelper.java +++ b/src/main/java/baritone/pathing/movement/MovementHelper.java @@ -331,11 +331,6 @@ static boolean isReplaceable(int x, int y, int z, BlockState state, BlockStateIn return state.getMaterial().isReplaceable(); } - @Deprecated - static boolean isReplacable(int x, int y, int z, BlockState state, BlockStateInterface bsi) { - return isReplaceable(x, y, z, state, bsi); - } - static boolean isDoorPassable(IPlayerContext ctx, BlockPos doorPos, BlockPos playerPos) { if (playerPos.equals(doorPos)) { return false; @@ -427,7 +422,7 @@ static Ternary canWalkOnBlockState(BlockState state) { if (block instanceof AzaleaBlock) { return YES; } - if (block == Blocks.LADDER || (block == Blocks.VINE && Baritone.settings().allowVines.value)) { // TODO reconsider this + if (block == Blocks.LADDER || (isClimbable(block) && Baritone.settings().allowVines.value)) { // TODO reconsider this return YES; } if (block == Blocks.FARMLAND || block == Blocks.DIRT_PATH || block == Blocks.SOUL_SAND) { @@ -528,7 +523,7 @@ static boolean canUseFrostWalker(IPlayerContext ctx, BlockPos pos) { */ static boolean mustBeSolidToWalkOn(CalculationContext context, int x, int y, int z, BlockState state) { Block block = state.getBlock(); - if (block == Blocks.LADDER || block == Blocks.VINE) { + if (isClimbable(block)) { return false; } if (!state.getFluidState().isEmpty()) { @@ -587,6 +582,20 @@ static boolean canPlaceAgainst(BlockStateInterface bsi, int x, int y, int z, Blo return isBlockNormalCube(state) || state.getBlock() == Blocks.GLASS || state.getBlock() instanceof StainedGlassBlock; } + /** + * Can we climb up this block by pressing space while inside it? + * Also doubles as "If I start a movement on this, can weird things happen?" + * because movements can end/start on these blocks despite them not being canWalkOn. + */ + static boolean isClimbable(Block block) { + return block == Blocks.LADDER + || block == Blocks.VINE + || block == Blocks.WEEPING_VINES + || block == Blocks.WEEPING_VINES_PLANT + || block == Blocks.TWISTING_VINES + || block == Blocks.TWISTING_VINES_PLANT; + } + static double getMiningDurationTicks(CalculationContext context, int x, int y, int z, boolean includeFalling) { return getMiningDurationTicks(context, x, y, z, context.get(x, y, z), includeFalling); } diff --git a/src/main/java/baritone/pathing/movement/movements/MovementAscend.java b/src/main/java/baritone/pathing/movement/movements/MovementAscend.java index 2ecd9d723..1b9fedea5 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementAscend.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementAscend.java @@ -112,7 +112,7 @@ public static double cost(CalculationContext context, int x, int y, int z, int d // and in that scenario, when we arrive and break srcUp2, that lets srcUp3 fall on us and suffocate us } BlockState srcDown = context.get(x, y - 1, z); - if (srcDown.getBlock() == Blocks.LADDER || srcDown.getBlock() == Blocks.VINE) { + if (MovementHelper.isClimbable(srcDown.getBlock())) { return COST_INF; } // we can jump from soul sand, but not from a bottom slab diff --git a/src/main/java/baritone/pathing/movement/movements/MovementDescend.java b/src/main/java/baritone/pathing/movement/movements/MovementDescend.java index 3e860e301..7f2672133 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementDescend.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementDescend.java @@ -94,7 +94,7 @@ public static void cost(CalculationContext context, int x, int y, int z, int des } Block fromDown = context.get(x, y - 1, z).getBlock(); - if (fromDown == Blocks.LADDER || fromDown == Blocks.VINE) { + if (MovementHelper.isClimbable(fromDown)) { return; } @@ -186,7 +186,7 @@ public static boolean dynamicFallCost(CalculationContext context, int x, int y, res.cost = tentativeCost; return false; } - if (unprotectedFallHeight <= 11 && (ontoBlock.getBlock() == Blocks.VINE || ontoBlock.getBlock() == Blocks.LADDER)) { + if (unprotectedFallHeight <= 11 && MovementHelper.isClimbable(ontoBlock.getBlock())) { // if fall height is greater than or equal to 11, we don't actually grab on to vines or ladders. the more you know // this effectively "resets" our falling speed costSoFar += FALL_N_BLOCKS_COST[unprotectedFallHeight - 1];// we fall until the top of this block (not including this block) diff --git a/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java b/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java index e7d74f03e..8be104758 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementDiagonal.java @@ -152,7 +152,7 @@ public static void cost(CalculationContext context, int x, int y, int z, int des multiplier += context.walkOnWaterOnePenalty * SQRT_2; } Block fromDownBlock = fromDown.getBlock(); - if (fromDownBlock == Blocks.LADDER || fromDownBlock == Blocks.VINE) { + if (MovementHelper.isClimbable(fromDownBlock)) { return; } if (fromDownBlock == Blocks.SOUL_SAND) { @@ -235,7 +235,7 @@ public static void cost(CalculationContext context, int x, int y, int z, int des } if (optionA != 0 || optionB != 0) { multiplier *= SQRT_2 - 0.001; // TODO tune - if (startIn == Blocks.LADDER || startIn == Blocks.VINE) { + if (MovementHelper.isClimbable(startIn)) { // edging around doesn't work if doing so would climb a ladder or vine instead of moving sideways return; } diff --git a/src/main/java/baritone/pathing/movement/movements/MovementParkour.java b/src/main/java/baritone/pathing/movement/movements/MovementParkour.java index 5ea1e4dcc..5936414c8 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementParkour.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementParkour.java @@ -91,7 +91,7 @@ public static void cost(CalculationContext context, int x, int y, int z, Directi return; } BlockState standingOn = context.get(x, y - 1, z); - if (standingOn.getBlock() == Blocks.VINE || standingOn.getBlock() == Blocks.LADDER || standingOn.getBlock() instanceof StairBlock || MovementHelper.isBottomSlab(standingOn)) { + if (MovementHelper.isClimbable(standingOn.getBlock()) || standingOn.getBlock() instanceof StairBlock || MovementHelper.isBottomSlab(standingOn)) { return; } // we can't jump from (frozen) water with assumeWalkOnWater because we can't be sure it will be frozen diff --git a/src/main/java/baritone/pathing/movement/movements/MovementPillar.java b/src/main/java/baritone/pathing/movement/movements/MovementPillar.java index 8007eb767..bbda3b982 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementPillar.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementPillar.java @@ -65,19 +65,16 @@ protected Set calculateValidPositions() { public static double cost(CalculationContext context, int x, int y, int z) { BlockState fromState = context.get(x, y, z); Block from = fromState.getBlock(); - boolean ladder = from == Blocks.LADDER || from == Blocks.VINE; + boolean ladder = MovementHelper.isClimbable(from); BlockState fromDown = context.get(x, y - 1, z); if (!ladder) { - if (fromDown.getBlock() == Blocks.LADDER || fromDown.getBlock() == Blocks.VINE) { + if (MovementHelper.isClimbable(fromDown.getBlock())) { return COST_INF; // can't pillar from a ladder or vine onto something that isn't also climbable } if (fromDown.getBlock() instanceof SlabBlock && fromDown.getValue(SlabBlock.TYPE) == SlabType.BOTTOM) { return COST_INF; // can't pillar up from a bottom slab onto a non ladder } } - if (from == Blocks.VINE && !hasAgainst(context, x, y, z)) { // TODO this vine can't be climbed, but we could place a pillar still since vines are replacable, no? perhaps the pillar jump would be impossible because of the slowdown actually. - return COST_INF; - } BlockState toBreak = context.get(x, y + 2, z); Block toBreakBlock = toBreak.getBlock(); if (toBreakBlock instanceof FenceGateBlock) { // see issue #172 @@ -116,7 +113,7 @@ public static double cost(CalculationContext context, int x, int y, int z) { return COST_INF; } if (hardness != 0) { - if (toBreakBlock == Blocks.LADDER || toBreakBlock == Blocks.VINE) { + if (MovementHelper.isClimbable(toBreakBlock)) { hardness = 0; // we won't actually need to break the ladder / vine because we're going to use it } else { BlockState check = context.get(x, y + 3, z); // the block on top of the one we're going to break, could it fall on us? @@ -145,29 +142,6 @@ public static double cost(CalculationContext context, int x, int y, int z) { } } - public static boolean hasAgainst(CalculationContext context, int x, int y, int z) { - return MovementHelper.isBlockNormalCube(context.get(x + 1, y, z)) || - MovementHelper.isBlockNormalCube(context.get(x - 1, y, z)) || - MovementHelper.isBlockNormalCube(context.get(x, y, z + 1)) || - MovementHelper.isBlockNormalCube(context.get(x, y, z - 1)); - } - - public static BlockPos getAgainst(CalculationContext context, BetterBlockPos vine) { - if (MovementHelper.isBlockNormalCube(context.get(vine.north()))) { - return vine.north(); - } - if (MovementHelper.isBlockNormalCube(context.get(vine.south()))) { - return vine.south(); - } - if (MovementHelper.isBlockNormalCube(context.get(vine.east()))) { - return vine.east(); - } - if (MovementHelper.isBlockNormalCube(context.get(vine.west()))) { - return vine.west(); - } - return null; - } - @Override public MovementState updateState(MovementState state) { super.updateState(state); @@ -192,8 +166,8 @@ public MovementState updateState(MovementState state) { } return state; } - boolean ladder = fromDown.getBlock() == Blocks.LADDER || fromDown.getBlock() == Blocks.VINE; - boolean vine = fromDown.getBlock() == Blocks.VINE; + boolean ladder = MovementHelper.isClimbable(fromDown.getBlock()); + Rotation rotation = RotationUtils.calcRotationFromVec3d(ctx.playerHead(), VecUtils.getBlockPosCenter(positionToPlace), ctx.playerRotations()); @@ -203,25 +177,12 @@ public MovementState updateState(MovementState state) { boolean blockIsThere = MovementHelper.canWalkOn(ctx, src) || ladder; if (ladder) { - BlockPos against = vine ? getAgainst(new CalculationContext(baritone), src) : src.relative(fromDown.getValue(LadderBlock.FACING).getOpposite()); - if (against == null) { - logDirect("Unable to climb vines. Consider disabling allowVines."); - return state.setStatus(MovementStatus.UNREACHABLE); - } - - if (ctx.playerFeet().equals(against.above()) || ctx.playerFeet().equals(dest)) { + if (ctx.playerFeet().equals(dest)) { return state.setStatus(MovementStatus.SUCCESS); } - if (MovementHelper.isBottomSlab(BlockStateInterface.get(ctx, src.below()))) { - state.setInput(Input.JUMP, true); - } - /* - if (thePlayer.getPosition0().getX() != from.getX() || thePlayer.getPosition0().getZ() != from.getZ()) { - Baritone.moveTowardsBlock(from); - } - */ - MovementHelper.moveTowards(ctx, state, against); + MovementHelper.moveTowards(ctx, state, dest); + state.setInput(Input.JUMP, true); return state; } else { // Get ready to place a throwaway block @@ -280,7 +241,7 @@ public MovementState updateState(MovementState state) { protected boolean prepared(MovementState state) { if (ctx.playerFeet().equals(src) || ctx.playerFeet().equals(src.below())) { Block block = BlockStateInterface.getBlock(ctx, src.below()); - if (block == Blocks.LADDER || block == Blocks.VINE) { + if (MovementHelper.isClimbable(block)) { state.setInput(Input.SNEAK, true); } } diff --git a/src/main/java/baritone/pathing/movement/movements/MovementTraverse.java b/src/main/java/baritone/pathing/movement/movements/MovementTraverse.java index 156da1adb..bbfd67d4c 100644 --- a/src/main/java/baritone/pathing/movement/movements/MovementTraverse.java +++ b/src/main/java/baritone/pathing/movement/movements/MovementTraverse.java @@ -118,13 +118,13 @@ public static double cost(CalculationContext context, int x, int y, int z, int d } return WC; } - if (srcDownBlock == Blocks.LADDER || srcDownBlock == Blocks.VINE) { + if (MovementHelper.isClimbable(srcDownBlock)) { hardness1 *= 5; hardness2 *= 5; } return WC + hardness1 + hardness2; } else {//this is a bridge, so we need to place a block - if (srcDownBlock == Blocks.LADDER || srcDownBlock == Blocks.VINE) { + if (MovementHelper.isClimbable(srcDownBlock)) { return COST_INF; } if (MovementHelper.isReplaceable(destX, y - 1, destZ, destOn, context.bsi)) { @@ -218,7 +218,7 @@ public MovementState updateState(MovementState state) { } Block fd = BlockStateInterface.get(ctx, src.below()).getBlock(); - boolean ladder = fd == Blocks.LADDER || fd == Blocks.VINE; + boolean ladder = MovementHelper.isClimbable(fd); //sneak may have been set to true in the PREPPING state while mining an adjacent block, but we still want it to be true if the player is about to go on magma state.setInput(Input.SNEAK, Baritone.settings().allowWalkOnMagmaBlocks.value && MovementHelper.steppingOnBlocks(ctx).stream().anyMatch(block -> ctx.world().getBlockState(block).is(Blocks.MAGMA_BLOCK))); @@ -265,7 +265,7 @@ public MovementState updateState(MovementState state) { } Block low = BlockStateInterface.get(ctx, src).getBlock(); Block high = BlockStateInterface.get(ctx, src.above()).getBlock(); - if (ctx.player().position().y > src.y + 0.1D && !ctx.player().isOnGround() && (low == Blocks.VINE || low == Blocks.LADDER || high == Blocks.VINE || high == Blocks.LADDER)) { + if (ctx.player().position().y > src.y + 0.1D && !ctx.player().isOnGround() && (MovementHelper.isClimbable(low) || MovementHelper.isClimbable(high))) { // hitting W could cause us to climb the ladder instead of going forward // wait until we're on the ground return state; @@ -278,15 +278,10 @@ public MovementState updateState(MovementState state) { } BlockState destDown = BlockStateInterface.get(ctx, dest.below()); - BlockPos against = positionsToBreak[0]; - if (feet.getY() != dest.getY() && ladder && (destDown.getBlock() == Blocks.VINE || destDown.getBlock() == Blocks.LADDER)) { - against = destDown.getBlock() == Blocks.VINE ? MovementPillar.getAgainst(new CalculationContext(baritone), dest.below()) : dest.relative(destDown.getValue(LadderBlock.FACING).getOpposite()); - if (against == null) { - logDirect("Unable to climb vines. Consider disabling allowVines."); - return state.setStatus(MovementStatus.UNREACHABLE); - } + if (feet.getY() != dest.getY() && ladder && MovementHelper.isClimbable(destDown.getBlock())) { + state.setInput(Input.JUMP, true); } - MovementHelper.moveTowards(ctx, state, against); + MovementHelper.moveTowards(ctx, state, positionsToBreak[0]); return state; } else { wasTheBridgeBlockAlwaysThere = false; @@ -373,7 +368,7 @@ public boolean safeToCancel(MovementState state) { protected boolean prepared(MovementState state) { if (ctx.playerFeet().equals(src) || ctx.playerFeet().equals(src.below())) { Block block = BlockStateInterface.getBlock(ctx, src.below()); - if (block == Blocks.LADDER || block == Blocks.VINE) { + if (MovementHelper.isClimbable(block)) { state.setInput(Input.SNEAK, true); } } diff --git a/src/main/java/baritone/process/elytra/ElytraBehavior.java b/src/main/java/baritone/process/elytra/ElytraBehavior.java index 6e78f4889..89729c9dd 100644 --- a/src/main/java/baritone/process/elytra/ElytraBehavior.java +++ b/src/main/java/baritone/process/elytra/ElytraBehavior.java @@ -1204,7 +1204,10 @@ private List simulate(final SolverContext context, final Vec3 goalDelta, f delta = delta.subtract(motion); // Collision box while the player is in motion, with additional padding for safety - final AABB inMotion = hitbox.move(motion.x, motion.y, motion.z).inflate(0.01); + // Use expandTowards for directional swept volume (fixes #5049) + // expandTowards handles negative vectors correctly (unlike inflate) + // and provides full swept volume coverage (unlike move) + final AABB inMotion = hitbox.expandTowards(motion.x, motion.y, motion.z).inflate(0.01); int xmin = fastFloor(inMotion.minX); int xmax = fastCeil(inMotion.maxX); diff --git a/src/test/java/baritone/process/elytra/ElytraHitboxTest.java b/src/test/java/baritone/process/elytra/ElytraHitboxTest.java new file mode 100644 index 000000000..83f8dbf2d --- /dev/null +++ b/src/test/java/baritone/process/elytra/ElytraHitboxTest.java @@ -0,0 +1,155 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package baritone.process.elytra; + +import net.minecraft.world.phys.AABB; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Tests for AABB collision volume behavior in elytra flight simulation. + * Verifies that expandTowards produces correct swept volumes for all + * motion vector directions, preventing both tunneling (#5049) and + * hitbox collapse (#5047). + */ +public class ElytraHitboxTest { + + /** + * Test swept volume with positive motion vectors. + * expandTowards(2, 0, 3) should extend the AABB in +X and +Z directions. + */ + @Test + public void testPositiveMotionVectors() { + AABB hitbox = new AABB(0, 0, 0, 0.6, 1.8, 0.6); + AABB expanded = hitbox.expandTowards(2, 0, 3); + + // Expanded box should cover start and end positions + assertTrue("minX should be at or below start", expanded.minX <= hitbox.minX); + assertTrue("maxX should be at or above end", expanded.maxX >= hitbox.maxX + 2); + assertTrue("minZ should be at or below start", expanded.minZ <= hitbox.minZ); + assertTrue("maxZ should be at or above end", expanded.maxZ >= hitbox.maxZ + 3); + + // Y should remain unchanged (zero motion) + assertTrue("minY should be unchanged", expanded.minY == hitbox.minY); + assertTrue("maxY should be unchanged", expanded.maxY == hitbox.maxY); + + // AABB invariants must hold + assertTrue("minX < maxX", expanded.minX < expanded.maxX); + assertTrue("minY < maxY", expanded.minY < expanded.maxY); + assertTrue("minZ < maxZ", expanded.minZ < expanded.maxZ); + } + + /** + * Test swept volume with zero motion on one axis. + * expandTowards(2, 0, 0) should only extend X, not Y or Z. + */ + @Test + public void testZeroMotionAxis() { + AABB hitbox = new AABB(0, 0, 0, 0.6, 1.8, 0.6); + AABB expanded = hitbox.expandTowards(2, 0, 0); + + // X should extend + assertTrue("maxX should extend", expanded.maxX >= hitbox.maxX + 2); + + // Y and Z should remain unchanged + assertTrue("minY unchanged", expanded.minY == hitbox.minY); + assertTrue("maxY unchanged", expanded.maxY == hitbox.maxY); + assertTrue("minZ unchanged", expanded.minZ == hitbox.minZ); + assertTrue("maxZ unchanged", expanded.maxZ == hitbox.maxZ); + + // AABB invariants + assertTrue("minX < maxX", expanded.minX < expanded.maxX); + assertTrue("minY < maxY", expanded.minY < expanded.maxY); + assertTrue("minZ < maxZ", expanded.minZ < expanded.maxZ); + } + + /** + * Test swept volume with negative motion vectors. + * expandTowards(-2, -1, -3) should extend toward negative coordinates + * without collapsing. This is the regression test for #5047. + */ + @Test + public void testNegativeMotionVectors() { + AABB hitbox = new AABB(5, 10, 5, 5.6, 11.8, 5.6); + AABB expanded = hitbox.expandTowards(-2, -1, -3); + + // Should extend toward negative + assertTrue("minX should decrease", expanded.minX < hitbox.minX); + assertTrue("minY should decrease", expanded.minY < hitbox.minY); + assertTrue("minZ should decrease", expanded.minZ < hitbox.minZ); + + // AABB invariants MUST hold — this is the critical regression test + assertTrue("minX < maxX (no collapse)", expanded.minX < expanded.maxX); + assertTrue("minY < maxY (no collapse)", expanded.minY < expanded.maxY); + assertTrue("minZ < maxZ (no collapse)", expanded.minZ < expanded.maxZ); + } + + /** + * Test swept volume with mixed positive/negative motion. + * expandTowards(2, -1, 0) should handle mixed axes correctly. + */ + @Test + public void testMixedMotionVectors() { + AABB hitbox = new AABB(0, 5, 0, 0.6, 6.8, 0.6); + AABB expanded = hitbox.expandTowards(2, -1, 0); + + // X extends positive + assertTrue("maxX extends positive", expanded.maxX >= hitbox.maxX + 2); + + // Y extends negative + assertTrue("minY extends negative", expanded.minY < hitbox.minY); + + // Z unchanged + assertTrue("minZ unchanged", expanded.minZ == hitbox.minZ); + assertTrue("maxZ unchanged", expanded.maxZ == hitbox.maxZ); + + // AABB invariants + assertTrue("minX < maxX", expanded.minX < expanded.maxX); + assertTrue("minY < maxY", expanded.minY < expanded.maxY); + assertTrue("minZ < maxZ", expanded.minZ < expanded.maxZ); + } + + /** + * Test that expandTowards with the inflate(0.01) safety padding + * produces valid AABBs for all motion directions. + * This matches the actual usage in ElytraBehavior.simulate(). + */ + @Test + public void testExpandTowardsWithSafetyPadding() { + AABB hitbox = new AABB(0, 0, 0, 0.6, 1.8, 0.6); + + // Positive motion + AABB posMotion = hitbox.expandTowards(2, 0, 3).inflate(0.01); + assertTrue("Positive: minX < maxX", posMotion.minX < posMotion.maxX); + assertTrue("Positive: minY < maxY", posMotion.minY < posMotion.maxY); + assertTrue("Positive: minZ < maxZ", posMotion.minZ < posMotion.maxZ); + + // Negative motion + AABB negMotion = hitbox.expandTowards(-2, -1, -3).inflate(0.01); + assertTrue("Negative: minX < maxX", negMotion.minX < negMotion.maxX); + assertTrue("Negative: minY < maxY", negMotion.minY < negMotion.maxY); + assertTrue("Negative: minZ < maxZ", negMotion.minZ < negMotion.maxZ); + + // Zero motion + AABB zeroMotion = hitbox.expandTowards(0, 0, 0).inflate(0.01); + assertTrue("Zero: minX < maxX", zeroMotion.minX < zeroMotion.maxX); + assertTrue("Zero: minY < maxY", zeroMotion.minY < zeroMotion.maxY); + assertTrue("Zero: minZ < maxZ", zeroMotion.minZ < zeroMotion.maxZ); + } +}