From f7c9419045470495fe76b0167d09d17c3cf4cc56 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 6 May 2025 10:58:48 -1000 Subject: [PATCH] pathfinder can now handle slabs, stairs, and dirt paths --- CHANGELOG.md | 1 + azalea/src/pathfinder/moves/basic.rs | 5 ++- azalea/src/pathfinder/moves/mod.rs | 13 +++++- azalea/src/pathfinder/moves/parkour.rs | 18 +++++++- azalea/src/pathfinder/world.rs | 59 ++++++++++++++++++++++++-- 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f2d23a..6cddfee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ write down most non-trivial breaking changes. - `StartJoinServerEvent` can now be used to join servers exclusively from the ECS without a Tokio runtime. - `FormattedText::to_html` and `FormattedText::to_custom_format`. - Add auto-reconnecting which is enabled by default. +- The pathfinder no longer avoids slabs, stairs, and dirt path blocks. ### Changed diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs index 4955ed08..d23bb894 100644 --- a/azalea/src/pathfinder/moves/basic.rs +++ b/azalea/src/pathfinder/moves/basic.rs @@ -135,7 +135,10 @@ fn execute_ascend_move(mut ctx: ExecuteCtx) { } if BlockPos::from(position) == start { - ctx.jump(); + // only jump if the target is more than 0.5 blocks above us + if target.y as f64 - position.y > 0.5 { + ctx.jump(); + } } } #[must_use] diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index 150dad52..f79f7249 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -191,10 +191,19 @@ pub struct IsReachedCtx<'a> { #[must_use] pub fn default_is_reached( IsReachedCtx { - position, target, .. + position, + target, + physics, + .. }: IsReachedCtx, ) -> bool { - BlockPos::from(position) == target + if BlockPos::from(position) == target { + return true; + } + + // this is to make it handle things like slabs correctly, if we're on the block + // below the target but on_ground + BlockPos::from(position).up(1) == target && physics.on_ground() } pub struct PathfinderCtx<'a> { diff --git a/azalea/src/pathfinder/moves/parkour.rs b/azalea/src/pathfinder/moves/parkour.rs index 1816a5e1..d4f136be 100644 --- a/azalea/src/pathfinder/moves/parkour.rs +++ b/azalea/src/pathfinder/moves/parkour.rs @@ -6,6 +6,11 @@ use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx}; use crate::pathfinder::{astar, costs::*, rel_block_pos::RelBlockPos}; pub fn parkour_move(ctx: &mut PathfinderCtx, node: RelBlockPos) { + if !ctx.world.is_block_solid(node.down(1)) { + // we can only parkour from solid blocks (not just standable blocks like slabs) + return; + } + parkour_forward_1_move(ctx, node); parkour_forward_2_move(ctx, node); parkour_forward_3_move(ctx, node); @@ -232,9 +237,18 @@ fn execute_parkour_move(mut ctx: ExecuteCtx) { #[must_use] pub fn parkour_is_reached( IsReachedCtx { - position, target, .. + position, + target, + physics, + .. }: IsReachedCtx, ) -> bool { // 0.094 and not 0 for lilypads - BlockPos::from(position) == target && (position.y - target.y as f64) < 0.094 + if BlockPos::from(position) == target && (position.y - target.y as f64) < 0.094 { + return true; + } + + // this is to make it handle things like slabs correctly, if we're on the block + // below the target but on_ground + BlockPos::from(position).up(1) == target && physics.on_ground() } diff --git a/azalea/src/pathfinder/world.rs b/azalea/src/pathfinder/world.rs index b89f0761..45d05810 100644 --- a/azalea/src/pathfinder/world.rs +++ b/azalea/src/pathfinder/world.rs @@ -81,8 +81,12 @@ impl CachedSections { pub struct CachedSection { pub pos: ChunkSectionPos, + /// Blocks that we can fully pass through (like air). pub passable_bitset: FixedBitSet<{ 4096_usize.div_ceil(8) }>, + /// Blocks that we can stand on and do parkour from. pub solid_bitset: FixedBitSet<{ 4096_usize.div_ceil(8) }>, + /// Blocks that we can stand on but might not be able to parkour from. + pub standable_bitset: FixedBitSet<{ 4096_usize.div_ceil(8) }>, } impl CachedWorld { @@ -193,6 +197,7 @@ impl CachedWorld { self.with_section(section_pos, |section| { let mut passable_bitset = FixedBitSet::<{ 4096_usize.div_ceil(8) }>::new(); let mut solid_bitset = FixedBitSet::<{ 4096_usize.div_ceil(8) }>::new(); + let mut standable_bitset = FixedBitSet::<{ 4096_usize.div_ceil(8) }>::new(); for i in 0..4096 { let block_state = section.get_at_index(i); if is_block_state_passable(block_state) { @@ -201,11 +206,15 @@ impl CachedWorld { if is_block_state_solid(block_state) { solid_bitset.set(i); } + if is_block_state_standable(block_state) { + standable_bitset.set(i); + } } CachedSection { pos: section_pos, passable_bitset, solid_bitset, + standable_bitset, } }) } @@ -235,6 +244,9 @@ impl CachedWorld { pub fn is_block_solid(&self, pos: RelBlockPos) -> bool { self.is_block_pos_solid(pos.apply(self.origin)) } + pub fn is_block_standable(&self, pos: RelBlockPos) -> bool { + self.is_block_pos_standable(pos.apply(self.origin)) + } fn is_block_pos_solid(&self, pos: BlockPos) -> bool { let (section_pos, section_block_pos) = @@ -253,6 +265,23 @@ impl CachedWorld { cached_blocks.insert(cached); solid } + fn is_block_pos_standable(&self, pos: BlockPos) -> bool { + let (section_pos, section_block_pos) = + (ChunkSectionPos::from(pos), ChunkSectionBlockPos::from(pos)); + let index = u16::from(section_block_pos) as usize; + // SAFETY: we're only accessing this from one thread + let cached_blocks = unsafe { &mut *self.cached_blocks.get() }; + if let Some(cached) = cached_blocks.get_mut(section_pos) { + return cached.standable_bitset.index(index); + } + + let Some(cached) = self.calculate_bitsets_for_section(section_pos) else { + return false; + }; + let solid = cached.standable_bitset.index(index); + cached_blocks.insert(cached); + solid + } /// Returns how much it costs to break this block. Returns 0 if the block is /// already passable. @@ -434,11 +463,11 @@ impl CachedWorld { self.is_standable_at_block_pos(pos.apply(self.origin)) } fn is_standable_at_block_pos(&self, pos: BlockPos) -> bool { - self.is_block_pos_solid(pos.down(1)) && self.is_passable_at_block_pos(pos) + self.is_block_pos_standable(pos.down(1)) && self.is_passable_at_block_pos(pos) } pub fn cost_for_standing(&self, pos: RelBlockPos, mining_cache: &MiningCache) -> f32 { - if !self.is_block_solid(pos.down(1)) { + if !self.is_block_standable(pos.down(1)) { return f32::INFINITY; } self.cost_for_passing(pos, mining_cache) @@ -501,7 +530,8 @@ pub fn is_block_state_passable(block: BlockState) -> bool { true } -/// whether this block has a solid hitbox (i.e. we can stand on it) +/// whether this block has a solid hitbox at the top (i.e. we can stand on it +/// and do parkour from it) pub fn is_block_state_solid(block: BlockState) -> bool { if block.is_air() { // fast path @@ -510,6 +540,29 @@ pub fn is_block_state_solid(block: BlockState) -> bool { block.is_collision_shape_full() } +pub fn is_block_state_standable(block: BlockState) -> bool { + if block.is_air() { + // fast path + return false; + } + if block.is_collision_shape_full() { + return true; + } + + let registry_block = azalea_registry::Block::from(block); + if azalea_registry::tags::blocks::SLABS.contains(®istry_block) + || azalea_registry::tags::blocks::STAIRS.contains(®istry_block) + { + return true; + } + + if registry_block == azalea_registry::Block::DirtPath { + return true; + } + + false +} + #[cfg(test)] mod tests {