From 7b442368daf29b43e20e122aac440e10d2fc7464 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 7 May 2025 07:09:48 +0900 Subject: [PATCH] fix some edge cases when pathfinding on slabs and stairs --- azalea/src/pathfinder/moves/basic.rs | 63 ++++++++++++++++++++++++++++ azalea/src/pathfinder/moves/mod.rs | 8 ++++ azalea/src/pathfinder/world.rs | 35 +++++++++++++++- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs index d23bb894..c7fccf26 100644 --- a/azalea/src/pathfinder/moves/basic.rs +++ b/azalea/src/pathfinder/moves/basic.rs @@ -1,5 +1,6 @@ use std::f32::consts::SQRT_2; +use azalea_block::{BlockState, properties}; use azalea_client::{SprintDirection, WalkDirection}; use azalea_core::{ direction::CardinalDirection, @@ -58,7 +59,34 @@ fn execute_forward_move(mut ctx: ExecuteCtx) { } fn ascend_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) { + // the block we're standing on must be solid (so we don't try to ascend from a + // bottom slab to a normal block in a way that's not possible) + + let is_unusual_shape = !ctx.world.is_block_solid(pos.down(1)); + let mut stair_facing = None; + + if is_unusual_shape { + // this is potentially expensive but it's rare enough that it shouldn't matter + // much + let block_below = ctx.world.get_block_state(pos.down(1)); + + let Some(found_stair_facing) = validate_stair_and_get_facing(block_below) else { + // return if it's not a stair or it's not facing the right way (like, if it's + // upside down or something) + return; + }; + + stair_facing = Some(found_stair_facing); + } + for dir in CardinalDirection::iter() { + if let Some(stair_facing) = stair_facing { + let expected_stair_facing = cardinal_direction_to_facing_property(dir); + if stair_facing != expected_stair_facing { + continue; + } + } + let offset = RelBlockPos::new(dir.x(), 1, dir.z()); let break_cost_1 = ctx @@ -134,6 +162,24 @@ fn execute_ascend_move(mut ctx: ExecuteCtx) { return; } + // if the target block is a stair that's facing in the direction we're going, we + // shouldn't jump + let block_below_target = ctx.get_block_state(target.down(1)); + if let Some(stair_facing) = validate_stair_and_get_facing(block_below_target) { + let expected_stair_facing = match (x_axis, z_axis) { + (0, 1) => Some(properties::FacingCardinal::North), + (1, 0) => Some(properties::FacingCardinal::East), + (0, -1) => Some(properties::FacingCardinal::South), + (-1, 0) => Some(properties::FacingCardinal::West), + _ => None, + }; + if let Some(expected_stair_facing) = expected_stair_facing { + if stair_facing == expected_stair_facing { + return; + } + } + } + if BlockPos::from(position) == start { // only jump if the target is more than 0.5 blocks above us if target.y as f64 - position.y > 0.5 { @@ -150,6 +196,23 @@ pub fn ascend_is_reached( BlockPos::from(position) == target || BlockPos::from(position) == target.down(1) } +fn validate_stair_and_get_facing(block_state: BlockState) -> Option { + let top_bottom = block_state.property::(); + if top_bottom != Some(properties::TopBottom::Bottom) { + return None; + } + + block_state.property::() +} +fn cardinal_direction_to_facing_property(dir: CardinalDirection) -> properties::FacingCardinal { + match dir { + CardinalDirection::North => properties::FacingCardinal::North, + CardinalDirection::East => properties::FacingCardinal::East, + CardinalDirection::South => properties::FacingCardinal::South, + CardinalDirection::West => properties::FacingCardinal::West, + } +} + fn descend_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) { for dir in CardinalDirection::iter() { let dir_delta = RelBlockPos::new(dir.x(), 0, dir.z()); diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index f79f7249..248f0a5c 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -3,6 +3,7 @@ pub mod parkour; use std::{fmt::Debug, sync::Arc}; +use azalea_block::BlockState; use azalea_client::{ SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection, inventory::SetSelectedHotbarSlotEvent, mining::StartMiningBlockEvent, @@ -175,6 +176,13 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { false } } + + pub fn get_block_state(&self, block: BlockPos) -> BlockState { + self.instance + .read() + .get_block_state(&block) + .unwrap_or_default() + } } pub struct IsReachedCtx<'a> { diff --git a/azalea/src/pathfinder/world.rs b/azalea/src/pathfinder/world.rs index 45d05810..3b1b36b9 100644 --- a/azalea/src/pathfinder/world.rs +++ b/azalea/src/pathfinder/world.rs @@ -3,7 +3,7 @@ use std::{ sync::Arc, }; -use azalea_block::BlockState; +use azalea_block::{BlockState, properties}; use azalea_core::{ bitset::FixedBitSet, position::{BlockPos, ChunkPos, ChunkSectionBlockPos, ChunkSectionPos}, @@ -241,6 +241,21 @@ impl CachedWorld { passable } + /// Get the block state at the given position. This is relatively slow, so + /// you should avoid it whenever possible. + pub fn get_block_state(&self, pos: RelBlockPos) -> BlockState { + self.get_block_state_at_pos(pos.apply(self.origin)) + } + + fn get_block_state_at_pos(&self, pos: BlockPos) -> BlockState { + let (section_pos, section_block_pos) = + (ChunkSectionPos::from(pos), ChunkSectionBlockPos::from(pos)); + let index = u16::from(section_block_pos) as usize; + + self.with_section(section_pos, |section| section.get_at_index(index)) + .unwrap_or_default() + } + pub fn is_block_solid(&self, pos: RelBlockPos) -> bool { self.is_block_pos_solid(pos.apply(self.origin)) } @@ -487,6 +502,10 @@ impl CachedWorld { } distance } + + pub fn origin(&self) -> BlockPos { + self.origin + } } /// whether this block is passable @@ -537,7 +556,19 @@ pub fn is_block_state_solid(block: BlockState) -> bool { // fast path return false; } - block.is_collision_shape_full() + if block.is_collision_shape_full() { + return true; + } + + if matches!( + block.property::(), + Some(properties::Type::Top | properties::Type::Double) + ) { + // top slabs + return true; + } + + false } pub fn is_block_state_standable(block: BlockState) -> bool {