aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2025-05-06 10:58:48 -1000
committermat <git@matdoes.dev>2025-05-07 06:59:22 +1000
commitf7c9419045470495fe76b0167d09d17c3cf4cc56 (patch)
treeb8bb1c4cc19fa9a6887224a57c43af9334ef285d
parentaf3affb467c01ee2880fbbc366ea0420c0580ab8 (diff)
downloadazalea-drasl-f7c9419045470495fe76b0167d09d17c3cf4cc56.tar.xz
pathfinder can now handle slabs, stairs, and dirt paths
-rw-r--r--CHANGELOG.md1
-rw-r--r--azalea/src/pathfinder/moves/basic.rs5
-rw-r--r--azalea/src/pathfinder/moves/mod.rs13
-rw-r--r--azalea/src/pathfinder/moves/parkour.rs18
-rw-r--r--azalea/src/pathfinder/world.rs59
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(&registry_block)
+ || azalea_registry::tags::blocks::STAIRS.contains(&registry_block)
+ {
+ return true;
+ }
+
+ if registry_block == azalea_registry::Block::DirtPath {
+ return true;
+ }
+
+ false
+}
+
#[cfg(test)]
mod tests {