aboutsummaryrefslogtreecommitdiff
path: root/azalea/src/pathfinder
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2023-10-02 17:51:38 -0500
committermat <git@matdoes.dev>2023-10-02 17:51:38 -0500
commitd0505f7de30e4a9a330ef99d0082849ee44723e0 (patch)
treef91879b0423eb0329efd2cb12a10a4e98b3b366d /azalea/src/pathfinder
parentc3d27487cae6af5d593193b922d1e81e93a1c45d (diff)
downloadazalea-drasl-d0505f7de30e4a9a330ef99d0082849ee44723e0.tar.xz
optimize pathfinder more
Diffstat (limited to 'azalea/src/pathfinder')
-rw-r--r--azalea/src/pathfinder/astar.rs4
-rw-r--r--azalea/src/pathfinder/mod.rs31
-rw-r--r--azalea/src/pathfinder/moves/basic.rs52
-rw-r--r--azalea/src/pathfinder/moves/mod.rs175
-rw-r--r--azalea/src/pathfinder/moves/parkour.rs54
5 files changed, 178 insertions, 138 deletions
diff --git a/azalea/src/pathfinder/astar.rs b/azalea/src/pathfinder/astar.rs
index 505b09c0..bc7b2309 100644
--- a/azalea/src/pathfinder/astar.rs
+++ b/azalea/src/pathfinder/astar.rs
@@ -26,14 +26,14 @@ const MIN_IMPROVEMENT: f32 = 0.01;
pub fn a_star<P, M, HeuristicFn, SuccessorsFn, SuccessFn>(
start: P,
heuristic: HeuristicFn,
- successors: SuccessorsFn,
+ mut successors: SuccessorsFn,
success: SuccessFn,
timeout: Duration,
) -> Path<P, M>
where
P: Eq + Hash + Copy + Debug,
HeuristicFn: Fn(P) -> f32,
- SuccessorsFn: Fn(P) -> Vec<Edge<P, M>>,
+ SuccessorsFn: FnMut(P) -> Vec<Edge<P, M>>,
SuccessFn: Fn(P) -> bool,
{
let start_time = Instant::now();
diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs
index 02b7d935..831a5524 100644
--- a/azalea/src/pathfinder/mod.rs
+++ b/azalea/src/pathfinder/mod.rs
@@ -19,6 +19,7 @@ use crate::ecs::{
query::{With, Without},
system::{Commands, Query, Res},
};
+use crate::pathfinder::moves::PathfinderCtx;
use azalea_client::movement::walk_listener;
use azalea_client::{StartSprintEvent, StartWalkEvent};
use azalea_core::position::BlockPos;
@@ -178,7 +179,8 @@ fn goto_listener(
debug!("start: {start:?}");
let world = &world_lock.read().chunks;
- let successors = |pos: BlockPos| successors_fn(world, pos);
+ let ctx = PathfinderCtx::new(world);
+ let successors = |pos: BlockPos| successors_fn(&ctx, pos);
let mut attempt_number = 0;
@@ -192,15 +194,20 @@ fn goto_listener(
|n| goal.heuristic(n),
successors,
|n| goal.success(n),
- Duration::from_secs(if attempt_number == 0 { 1 } else { 5 }),
+ Duration::from_secs(if attempt_number == 0 { 10 } else { 10 }),
);
let end_time = std::time::Instant::now();
debug!("partial: {partial:?}");
- debug!("time: {:?}", end_time - start_time);
+ let duration = end_time - start_time;
+ if partial {
+ info!("Pathfinder took {duration:?} (timed out)");
+ } else {
+ info!("Pathfinder took {duration:?}");
+ }
- info!("Path:");
+ debug!("Path:");
for movement in &movements {
- info!(" {:?}", movement.target);
+ debug!(" {:?}", movement.target);
}
path = movements.into_iter().collect::<VecDeque<_>>();
@@ -275,11 +282,10 @@ fn path_found_listener(
let world_lock = instance_container.get(instance_name).expect(
"Entity tried to pathfind but the entity isn't in a valid world",
);
+ let world = &world_lock.read().chunks;
+ let ctx = PathfinderCtx::new(&world);
let successors_fn: moves::SuccessorsFn = event.successors_fn;
- let successors = |pos: BlockPos| {
- let world = &world_lock.read().chunks;
- successors_fn(world, pos)
- };
+ let successors = |pos: BlockPos| successors_fn(&ctx, pos);
if successors(last_node.target)
.iter()
@@ -439,10 +445,9 @@ fn tick_execute_path(
{
// obstruction check (the path we're executing isn't possible anymore)
- let successors = |pos: BlockPos| {
- let world = &world_lock.read().chunks;
- successors_fn(world, pos)
- };
+ let world = &world_lock.read().chunks;
+ let ctx = PathfinderCtx::new(&world);
+ let successors = |pos: BlockPos| successors_fn(&ctx, pos);
if let Some(last_reached_node) = pathfinder.last_reached_node {
if let Some(obstructed_index) =
diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs
index 1785ec2e..b7bb1116 100644
--- a/azalea/src/pathfinder/moves/basic.rs
+++ b/azalea/src/pathfinder/moves/basic.rs
@@ -5,33 +5,29 @@ use azalea_core::{
direction::CardinalDirection,
position::{BlockPos, Vec3},
};
-use azalea_world::ChunkStorage;
use crate::{
pathfinder::{astar, costs::*},
JumpEvent, LookAtEvent,
};
-use super::{
- default_is_reached, fall_distance, is_block_passable, is_passable, is_standable, Edge,
- ExecuteCtx, IsReachedCtx, MoveData,
-};
+use super::{default_is_reached, Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
-pub fn basic_move(world: &ChunkStorage, node: BlockPos) -> Vec<Edge> {
+pub fn basic_move(ctx: &PathfinderCtx, node: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
- edges.extend(forward_move(world, node));
- edges.extend(ascend_move(world, node));
- edges.extend(descend_move(world, node));
- edges.extend(diagonal_move(world, node));
+ edges.extend(forward_move(ctx, node));
+ edges.extend(ascend_move(ctx, node));
+ edges.extend(descend_move(ctx, node));
+ edges.extend(diagonal_move(ctx, node));
edges
}
-fn forward_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn forward_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let offset = BlockPos::new(dir.x(), 0, dir.z());
- if !is_standable(&(pos + offset), world) {
+ if !ctx.is_standable(&(pos + offset)) {
continue;
}
@@ -72,15 +68,15 @@ fn execute_forward_move(
});
}
-fn ascend_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn ascend_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let offset = BlockPos::new(dir.x(), 1, dir.z());
- if !is_block_passable(&pos.up(2), world) {
+ if !ctx.is_block_passable(&pos.up(2)) {
continue;
}
- if !is_standable(&(pos + offset), world) {
+ if !ctx.is_standable(&(pos + offset)) {
continue;
}
@@ -156,23 +152,23 @@ pub fn ascend_is_reached(
BlockPos::from(position) == target || BlockPos::from(position) == target.down(1)
}
-fn descend_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn descend_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let dir_delta = BlockPos::new(dir.x(), 0, dir.z());
let new_horizontal_position = pos + dir_delta;
- let fall_distance = fall_distance(&new_horizontal_position, world);
+ let fall_distance = ctx.fall_distance(&new_horizontal_position);
if fall_distance == 0 || fall_distance > 3 {
continue;
}
let new_position = new_horizontal_position.down(fall_distance as i32);
// check whether 3 blocks vertically forward are passable
- if !is_passable(&new_horizontal_position, world) {
+ if !ctx.is_passable(&new_horizontal_position) {
continue;
}
// check whether we can stand on the target position
- if !is_standable(&new_position, world) {
+ if !ctx.is_standable(&new_position) {
continue;
}
@@ -258,22 +254,22 @@ pub fn descend_is_reached(
&& (position.y - target.y as f64) < 0.5
}
-fn diagonal_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn diagonal_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let right = dir.right();
let offset = BlockPos::new(dir.x() + right.x(), 0, dir.z() + right.z());
- if !is_passable(
- &BlockPos::new(pos.x + dir.x(), pos.y, pos.z + dir.z()),
- world,
- ) && !is_passable(
- &BlockPos::new(pos.x + dir.right().x(), pos.y, pos.z + dir.right().z()),
- world,
- ) {
+ if !ctx.is_passable(&BlockPos::new(pos.x + dir.x(), pos.y, pos.z + dir.z()))
+ && !ctx.is_passable(&BlockPos::new(
+ pos.x + dir.right().x(),
+ pos.y,
+ pos.z + dir.right().z(),
+ ))
+ {
continue;
}
- if !is_standable(&(pos + offset), world) {
+ if !ctx.is_standable(&(pos + offset)) {
continue;
}
// +0.001 so it doesn't unnecessarily go diagonal sometimes
diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs
index 197229fd..caf4bbb4 100644
--- a/azalea/src/pathfinder/moves/mod.rs
+++ b/azalea/src/pathfinder/moves/mod.rs
@@ -1,21 +1,22 @@
pub mod basic;
pub mod parkour;
-use std::fmt::Debug;
+use std::{cell::RefCell, fmt::Debug};
use crate::{JumpEvent, LookAtEvent};
use super::astar;
+use azalea_block::BlockState;
use azalea_client::{StartSprintEvent, StartWalkEvent};
-use azalea_core::position::{BlockPos, Vec3};
+use azalea_core::position::{BlockPos, ChunkBlockPos, ChunkPos, Vec3};
use azalea_physics::collision::{self, BlockWithShape};
use azalea_world::ChunkStorage;
use bevy_ecs::{entity::Entity, event::EventWriter};
+use nohash_hasher::IntMap;
type Edge = astar::Edge<BlockPos, MoveData>;
-pub type SuccessorsFn =
- fn(&azalea_world::ChunkStorage, BlockPos) -> Vec<astar::Edge<BlockPos, MoveData>>;
+pub type SuccessorsFn = fn(&PathfinderCtx, BlockPos) -> Vec<astar::Edge<BlockPos, MoveData>>;
#[derive(Clone)]
pub struct MoveData {
@@ -33,71 +34,110 @@ impl Debug for MoveData {
}
}
-/// whether this block is passable
-fn is_block_passable(pos: &BlockPos, world: &ChunkStorage) -> bool {
- let Some(block) = world.get_block_state(pos) else {
- return false;
- };
- if block.is_air() {
- // fast path
- return true;
- }
- if block.shape() != &*collision::EMPTY_SHAPE {
- return false;
- }
- if block == azalea_registry::Block::Water.into() {
- return false;
- }
- if block.waterlogged() {
- return false;
+pub struct PathfinderCtx<'a> {
+ world: &'a ChunkStorage,
+ cached_chunks: RefCell<IntMap<ChunkPos, Vec<azalea_world::Section>>>,
+}
+
+impl<'a> PathfinderCtx<'a> {
+ pub fn new(world: &'a ChunkStorage) -> Self {
+ Self {
+ world,
+ cached_chunks: Default::default(),
+ }
}
- // block.waterlogged currently doesn't account for seagrass and some other water
- // blocks
- if block == azalea_registry::Block::Seagrass.into() {
- return false;
+
+ fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
+ let chunk_pos = ChunkPos::from(pos);
+
+ let mut cached_chunks = self.cached_chunks.borrow_mut();
+ if let Some(sections) = cached_chunks.get(&chunk_pos) {
+ return azalea_world::chunk_storage::get_block_state_from_sections(
+ sections,
+ &ChunkBlockPos::from(pos),
+ self.world.min_y,
+ );
+ }
+
+ let chunk = self.world.get(&chunk_pos)?;
+ let chunk = chunk.read();
+
+ cached_chunks.insert(chunk_pos, chunk.sections.clone());
+
+ azalea_world::chunk_storage::get_block_state_from_sections(
+ &chunk.sections,
+ &ChunkBlockPos::from(pos),
+ self.world.min_y,
+ )
}
- true
-}
+ /// whether this block is passable
+ pub fn is_block_passable(&self, pos: &BlockPos) -> bool {
+ let Some(block) = self.get_block_state(pos) else {
+ return false;
+ };
+ if block.is_air() {
+ // fast path
+ return true;
+ }
+ if block.shape() != &*collision::EMPTY_SHAPE {
+ return false;
+ }
+ if block == azalea_registry::Block::Water.into() {
+ return false;
+ }
+ if block.waterlogged() {
+ return false;
+ }
+ // block.waterlogged currently doesn't account for seagrass and some other water
+ // blocks
+ if block == azalea_registry::Block::Seagrass.into() {
+ return false;
+ }
-/// whether this block has a solid hitbox (i.e. we can stand on it)
-fn is_block_solid(pos: &BlockPos, world: &ChunkStorage) -> bool {
- let Some(block) = world.get_block_state(pos) else {
- return false;
- };
- if block.is_air() {
- // fast path
- return false;
+ true
}
- block.shape() == &*collision::BLOCK_SHAPE
-}
-/// Whether this block and the block above are passable
-fn is_passable(pos: &BlockPos, world: &ChunkStorage) -> bool {
- is_block_passable(pos, world) && is_block_passable(&pos.up(1), world)
-}
+ /// whether this block has a solid hitbox (i.e. we can stand on it)
+ pub fn is_block_solid(&self, pos: &BlockPos) -> bool {
+ let Some(block) = self.get_block_state(pos) else {
+ return false;
+ };
+ if block.is_air() {
+ // fast path
+ return false;
+ }
+ block.shape() == &*collision::BLOCK_SHAPE
+ }
-/// Whether we can stand in this position. Checks if the block below is solid,
-/// and that the two blocks above that are passable.
+ /// Whether this block and the block above are passable
+ pub fn is_passable(&self, pos: &BlockPos) -> bool {
+ self.is_block_passable(pos) && self.is_block_passable(&pos.up(1))
+ }
-fn is_standable(pos: &BlockPos, world: &ChunkStorage) -> bool {
- is_block_solid(&pos.down(1), world) && is_passable(pos, world)
-}
+ /// Whether we can stand in this position. Checks if the block below is
+ /// solid, and that the two blocks above that are passable.
-/// Get the amount of air blocks until the next solid block below this one.
-fn fall_distance(pos: &BlockPos, world: &ChunkStorage) -> u32 {
- let mut distance = 0;
- let mut current_pos = pos.down(1);
- while is_block_passable(&current_pos, world) {
- distance += 1;
- current_pos = current_pos.down(1);
+ pub fn is_standable(&self, pos: &BlockPos) -> bool {
+ self.is_block_solid(&pos.down(1)) && self.is_passable(pos)
+ }
+
+ /// Get the amount of air blocks until the next solid block below this one.
+ pub fn fall_distance(&self, pos: &BlockPos) -> u32 {
+ let mut distance = 0;
+ let mut current_pos = pos.down(1);
+ while self.is_block_passable(&current_pos) {
+ distance += 1;
+ current_pos = current_pos.down(1);
- if current_pos.y < world.min_y {
- return u32::MAX;
+ if current_pos.y < self.world.min_y {
+ return u32::MAX;
+ }
}
+ distance
}
- distance
}
+
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
pub entity: Entity,
/// The node that we're trying to reach.
@@ -121,10 +161,10 @@ pub struct IsReachedCtx<'a> {
pub physics: &'a azalea_entity::Physics,
}
-pub fn default_move(world: &ChunkStorage, node: BlockPos) -> Vec<Edge> {
+pub fn default_move(ctx: &PathfinderCtx, node: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
- edges.extend(basic::basic_move(world, node));
- edges.extend(parkour::parkour_move(world, node));
+ edges.extend(basic::basic_move(ctx, node));
+ edges.extend(parkour::parkour_move(ctx, node));
edges
}
@@ -163,8 +203,9 @@ mod tests {
.chunks
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
- assert!(!is_block_passable(&BlockPos::new(0, 0, 0), &world));
- assert!(is_block_passable(&BlockPos::new(0, 1, 0), &world));
+ let ctx = PathfinderCtx::new(&world);
+ assert!(!ctx.is_block_passable(&BlockPos::new(0, 0, 0)));
+ assert!(ctx.is_block_passable(&BlockPos::new(0, 1, 0),));
}
#[test]
@@ -183,8 +224,9 @@ mod tests {
.chunks
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
- assert!(is_block_solid(&BlockPos::new(0, 0, 0), &world));
- assert!(!is_block_solid(&BlockPos::new(0, 1, 0), &world));
+ let ctx = PathfinderCtx::new(&world);
+ assert!(ctx.is_block_solid(&BlockPos::new(0, 0, 0)));
+ assert!(!ctx.is_block_solid(&BlockPos::new(0, 1, 0)));
}
#[test]
@@ -209,8 +251,9 @@ mod tests {
.chunks
.set_block_state(&BlockPos::new(0, 3, 0), BlockState::AIR, &world);
- assert!(is_standable(&BlockPos::new(0, 1, 0), &world));
- assert!(!is_standable(&BlockPos::new(0, 0, 0), &world));
- assert!(!is_standable(&BlockPos::new(0, 2, 0), &world));
+ let ctx = PathfinderCtx::new(&world);
+ assert!(ctx.is_standable(&BlockPos::new(0, 1, 0)));
+ assert!(!ctx.is_standable(&BlockPos::new(0, 0, 0)));
+ assert!(!ctx.is_standable(&BlockPos::new(0, 2, 0)));
}
}
diff --git a/azalea/src/pathfinder/moves/parkour.rs b/azalea/src/pathfinder/moves/parkour.rs
index 03635faa..53f5a348 100644
--- a/azalea/src/pathfinder/moves/parkour.rs
+++ b/azalea/src/pathfinder/moves/parkour.rs
@@ -1,46 +1,42 @@
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
use azalea_core::{direction::CardinalDirection, position::BlockPos};
-use azalea_world::ChunkStorage;
use crate::{
pathfinder::{astar, costs::*},
JumpEvent, LookAtEvent,
};
-use super::{
- default_is_reached, is_block_passable, is_block_solid, is_passable, is_standable, Edge,
- ExecuteCtx, IsReachedCtx, MoveData,
-};
+use super::{default_is_reached, Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
-pub fn parkour_move(world: &ChunkStorage, node: BlockPos) -> Vec<Edge> {
+pub fn parkour_move(ctx: &PathfinderCtx, node: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
- edges.extend(parkour_forward_1_move(world, node));
- edges.extend(parkour_headhitter_forward_1_move(world, node));
- edges.extend(parkour_forward_2_move(world, node));
+ edges.extend(parkour_forward_1_move(ctx, node));
+ edges.extend(parkour_headhitter_forward_1_move(ctx, node));
+ edges.extend(parkour_forward_2_move(ctx, node));
edges
}
-fn parkour_forward_1_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn parkour_forward_1_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let gap_offset = BlockPos::new(dir.x(), 0, dir.z());
let offset = BlockPos::new(dir.x() * 2, 0, dir.z() * 2);
- if !is_standable(&(pos + offset), world) {
+ if !ctx.is_standable(&(pos + offset)) {
continue;
}
- if !is_passable(&(pos + gap_offset), world) {
+ if !ctx.is_passable(&(pos + gap_offset)) {
continue;
}
- if !is_block_passable(&(pos + gap_offset).up(2), world) {
+ if !ctx.is_block_passable(&(pos + gap_offset).up(2)) {
continue;
}
// make sure we actually have to jump
- if is_block_solid(&(pos + gap_offset).down(1), world) {
+ if ctx.is_block_solid(&(pos + gap_offset).down(1)) {
continue;
}
// make sure it's not a headhitter
- if !is_block_passable(&pos.up(2), world) {
+ if !ctx.is_block_passable(&pos.up(2)) {
continue;
}
@@ -61,34 +57,34 @@ fn parkour_forward_1_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
edges
}
-fn parkour_forward_2_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn parkour_forward_2_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let gap_1_offset = BlockPos::new(dir.x(), 0, dir.z());
let gap_2_offset = BlockPos::new(dir.x() * 2, 0, dir.z() * 2);
let offset = BlockPos::new(dir.x() * 3, 0, dir.z() * 3);
- if !is_standable(&(pos + offset), world) {
+ if !ctx.is_standable(&(pos + offset)) {
continue;
}
- if !is_passable(&(pos + gap_1_offset), world) {
+ if !ctx.is_passable(&(pos + gap_1_offset)) {
continue;
}
- if !is_block_passable(&(pos + gap_1_offset).up(2), world) {
+ if !ctx.is_block_passable(&(pos + gap_1_offset).up(2)) {
continue;
}
- if !is_passable(&(pos + gap_2_offset), world) {
+ if !ctx.is_passable(&(pos + gap_2_offset)) {
continue;
}
- if !is_block_passable(&(pos + gap_2_offset).up(2), world) {
+ if !ctx.is_block_passable(&(pos + gap_2_offset).up(2)) {
continue;
}
// make sure we actually have to jump
- if is_block_solid(&(pos + gap_1_offset).down(1), world) {
+ if ctx.is_block_solid(&(pos + gap_1_offset).down(1)) {
continue;
}
// make sure it's not a headhitter
- if !is_block_passable(&pos.up(2), world) {
+ if !ctx.is_block_passable(&pos.up(2)) {
continue;
}
@@ -112,27 +108,27 @@ fn parkour_forward_2_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
edges
}
-fn parkour_headhitter_forward_1_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
+fn parkour_headhitter_forward_1_move(ctx: &PathfinderCtx, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let gap_offset = BlockPos::new(dir.x(), 0, dir.z());
let offset = BlockPos::new(dir.x() * 2, 0, dir.z() * 2);
- if !is_standable(&(pos + offset), world) {
+ if !ctx.is_standable(&(pos + offset)) {
continue;
}
- if !is_passable(&(pos + gap_offset), world) {
+ if !ctx.is_passable(&(pos + gap_offset)) {
continue;
}
- if !is_block_passable(&(pos + gap_offset).up(2), world) {
+ if !ctx.is_block_passable(&(pos + gap_offset).up(2)) {
continue;
}
// make sure we actually have to jump
- if is_block_solid(&(pos + gap_offset).down(1), world) {
+ if ctx.is_block_solid(&(pos + gap_offset).down(1)) {
continue;
}
// make sure it is a headhitter
- if !is_block_solid(&pos.up(2), world) {
+ if !ctx.is_block_solid(&pos.up(2)) {
continue;
}