simplify pathfinder more

This commit is contained in:
mat 2023-08-26 18:08:13 -05:00
parent 5d7669f72b
commit dea717b68e
4 changed files with 397 additions and 457 deletions

View file

@ -5,7 +5,6 @@ pub mod simulation;
use crate::bot::{JumpEvent, LookAtEvent};
use crate::pathfinder::astar::a_star;
use crate::pathfinder::moves::DefaultMoves;
use crate::WalkDirection;
use crate::app::{App, Plugin};
@ -18,7 +17,7 @@ use crate::ecs::{
};
use azalea_client::movement::walk_listener;
use azalea_client::{StartSprintEvent, StartWalkEvent};
use azalea_core::{BlockPos, CardinalDirection};
use azalea_core::BlockPos;
use azalea_entity::metadata::Player;
use azalea_entity::Local;
use azalea_entity::{Physics, Position};
@ -119,6 +118,8 @@ fn goto_listener(
mut query: Query<(&Position, &InstanceName)>,
instance_container: Res<InstanceContainer>,
) {
let successors_fn = moves::basic::basic_move;
let thread_pool = AsyncComputeTaskPool::get();
for event in events.iter() {
@ -138,40 +139,9 @@ fn goto_listener(
let task = thread_pool.spawn(async move {
debug!("start: {start:?}, end: {end:?}");
let possible_moves: Vec<DefaultMoves> = vec![
DefaultMoves::Forward(CardinalDirection::North),
DefaultMoves::Forward(CardinalDirection::East),
DefaultMoves::Forward(CardinalDirection::South),
DefaultMoves::Forward(CardinalDirection::West),
//
DefaultMoves::Ascend(CardinalDirection::North),
DefaultMoves::Ascend(CardinalDirection::East),
DefaultMoves::Ascend(CardinalDirection::South),
DefaultMoves::Ascend(CardinalDirection::West),
//
DefaultMoves::Descend(CardinalDirection::North),
DefaultMoves::Descend(CardinalDirection::East),
DefaultMoves::Descend(CardinalDirection::South),
DefaultMoves::Descend(CardinalDirection::West),
//
DefaultMoves::Diagonal(CardinalDirection::North),
DefaultMoves::Diagonal(CardinalDirection::East),
DefaultMoves::Diagonal(CardinalDirection::South),
DefaultMoves::Diagonal(CardinalDirection::West),
];
let successors = |pos: BlockPos| {
let mut edges = Vec::new();
let world = world_lock.read();
for possible_move in &possible_moves {
let move_result = possible_move.get(&world, pos);
if let Some(edge) = move_result {
edges.push(edge);
}
}
edges
successors_fn(&world, pos)
};
let start_time = std::time::Instant::now();
@ -183,7 +153,6 @@ fn goto_listener(
Duration::from_secs(1),
);
let end_time = std::time::Instant::now();
debug!("movements: {movements:?}");
debug!("partial: {partial:?}");
debug!("time: {:?}", end_time - start_time);
@ -268,8 +237,8 @@ fn tick_execute_path(
walk_events: &mut walk_events,
jump_events: &mut jump_events,
};
trace!("executing move {:?}", movement.data.move_kind);
movement.data.move_kind.execute(ctx);
trace!("executing move");
(movement.data.execute)(ctx);
break;
}
}

View file

@ -1,420 +0,0 @@
use crate::{JumpEvent, LookAtEvent};
use super::astar;
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent};
use azalea_core::{BlockPos, CardinalDirection, Vec3};
use azalea_physics::collision::{self, BlockWithShape};
use azalea_world::Instance;
use bevy_ecs::{entity::Entity, event::EventWriter};
type Edge = astar::Edge<BlockPos, MoveData>;
#[derive(Debug, Clone)]
pub struct MoveData {
pub move_kind: DefaultMoves,
}
/// whether this block is passable
fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
if let Some(block) = world.chunks.get_block_state(pos) {
if block.shape() != &collision::empty_shape() {
return false;
}
if block == azalea_registry::Block::Water.into() {
return false;
}
if block.waterlogged() {
return false;
}
block.shape() == &collision::empty_shape()
} else {
false
}
}
/// whether this block has a solid hitbox (i.e. we can stand on it)
fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
if let Some(block) = world.chunks.get_block_state(pos) {
block.shape() == &collision::block_shape()
} else {
false
}
}
/// Whether this block and the block above are passable
fn is_passable(pos: &BlockPos, world: &Instance) -> bool {
is_block_passable(pos, world) && is_block_passable(&pos.up(1), world)
}
/// Whether we can stand in this position. Checks if the block below is solid,
/// and that the two blocks above that are passable.
fn is_standable(pos: &BlockPos, world: &Instance) -> bool {
is_block_solid(&pos.down(1), world) && is_passable(pos, world)
}
/// Get the amount of air blocks until the next solid block below this one.
fn fall_distance(pos: &BlockPos, world: &Instance) -> 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);
if current_pos.y < world.chunks.min_y {
return u32::MAX;
}
}
distance
}
const JUMP_COST: f32 = 0.5;
const WALK_ONE_BLOCK_COST: f32 = 1.0;
const FALL_ONE_BLOCK_COST: f32 = 0.5;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum DefaultMoves {
Forward(CardinalDirection),
Ascend(CardinalDirection),
Descend(CardinalDirection),
Diagonal(CardinalDirection),
}
impl DefaultMoves {
pub fn get(self, world: &Instance, node: BlockPos) -> Option<Edge> {
match self {
DefaultMoves::Forward(dir) => ForwardMove(dir).get(world, node),
DefaultMoves::Ascend(dir) => AscendMove(dir).get(world, node),
DefaultMoves::Descend(dir) => DescendMove(dir).get(world, node),
DefaultMoves::Diagonal(dir) => DiagonalMove(dir).get(world, node),
}
}
pub fn execute(self, ctx: ExecuteCtx) {
match self {
DefaultMoves::Forward(_) => ForwardMove::execute(ctx),
DefaultMoves::Ascend(_) => AscendMove::execute(ctx),
DefaultMoves::Descend(_) => DescendMove::execute(ctx),
DefaultMoves::Diagonal(_) => DiagonalMove::execute(ctx),
}
}
}
pub trait MoveImpl: Send + Sync {
fn get(&self, world: &Instance, node: BlockPos) -> Option<Edge>;
fn execute(ctx: ExecuteCtx);
}
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
pub entity: Entity,
pub target: BlockPos,
pub position: Vec3,
pub look_at_events: &'a mut EventWriter<'w1, LookAtEvent>,
pub sprint_events: &'a mut EventWriter<'w2, StartSprintEvent>,
pub walk_events: &'a mut EventWriter<'w3, StartWalkEvent>,
pub jump_events: &'a mut EventWriter<'w4, JumpEvent>,
}
pub struct ForwardMove(pub CardinalDirection);
impl MoveImpl for ForwardMove {
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
let offset = BlockPos::new(self.0.x(), 0, self.0.z());
if !is_standable(&(pos + offset), world) {
return None;
}
let cost = WALK_ONE_BLOCK_COST;
Some(Edge {
movement: astar::Movement {
target: pos + offset,
data: MoveData {
move_kind: DefaultMoves::Forward(self.0),
},
},
cost,
})
}
fn execute(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
}
pub struct AscendMove(pub CardinalDirection);
impl MoveImpl for AscendMove {
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
let offset = BlockPos::new(self.0.x(), 1, self.0.z());
if !is_block_passable(&pos.up(2), world) || !is_standable(&(pos + offset), world) {
return None;
}
let cost = WALK_ONE_BLOCK_COST + JUMP_COST;
// Some(MoveResult {
// node: Node {
// pos: node.pos + offset,
// vertical_vel: VerticalVel::None,
// },
// cost,
// })
Some(Edge {
movement: astar::Movement {
target: pos + offset,
data: MoveData {
move_kind: DefaultMoves::Ascend(self.0),
},
},
cost,
})
}
fn execute(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
jump_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
jump_events.send(JumpEvent { entity });
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
}
pub struct DescendMove(pub CardinalDirection);
impl MoveImpl for DescendMove {
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
let new_horizontal_position = pos + BlockPos::new(self.0.x(), 0, self.0.z());
let fall_distance = fall_distance(&new_horizontal_position, world);
if fall_distance == 0 {
return None;
}
if fall_distance > 3 {
return None;
}
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) {
return None;
}
let cost = WALK_ONE_BLOCK_COST + FALL_ONE_BLOCK_COST * fall_distance as f32;
Some(Edge {
movement: astar::Movement {
target: new_position,
data: MoveData {
move_kind: DefaultMoves::Descend(self.0),
},
},
cost,
})
}
fn execute(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
}
pub struct DiagonalMove(pub CardinalDirection);
impl MoveImpl for DiagonalMove {
fn get(&self, world: &Instance, pos: BlockPos) -> Option<Edge> {
let right = self.0.right();
let offset = BlockPos::new(self.0.x() + right.x(), 0, self.0.z() + right.z());
if !is_passable(
&BlockPos::new(pos.x + self.0.x(), pos.y, pos.z + self.0.z()),
world,
) && !is_passable(
&BlockPos::new(
pos.x + self.0.right().x(),
pos.y,
pos.z + self.0.right().z(),
),
world,
) {
return None;
}
if !is_standable(&(pos + offset), world) {
return None;
}
let cost = WALK_ONE_BLOCK_COST * 1.4;
// Some(MoveResult {
// node: Node {
// pos: node.pos + offset,
// vertical_vel: VerticalVel::None,
// },
// cost,
// })
Some(Edge {
movement: astar::Movement {
target: pos + offset,
data: MoveData {
move_kind: DefaultMoves::Diagonal(self.0),
},
},
cost,
})
}
fn execute(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use azalea_block::BlockState;
use azalea_core::ChunkPos;
use azalea_world::{Chunk, ChunkStorage, PartialInstance};
#[test]
fn test_is_passable() {
let mut partial_world = PartialInstance::default();
let mut chunk_storage = ChunkStorage::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 0, 0),
azalea_registry::Block::Stone.into(),
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 1, 0),
BlockState::AIR,
&chunk_storage,
);
let world = chunk_storage.into();
assert!(!is_block_passable(&BlockPos::new(0, 0, 0), &world));
assert!(is_block_passable(&BlockPos::new(0, 1, 0), &world));
}
#[test]
fn test_is_solid() {
let mut partial_world = PartialInstance::default();
let mut chunk_storage = ChunkStorage::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 0, 0),
azalea_registry::Block::Stone.into(),
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 1, 0),
BlockState::AIR,
&chunk_storage,
);
let world = chunk_storage.into();
assert!(is_block_solid(&BlockPos::new(0, 0, 0), &world));
assert!(!is_block_solid(&BlockPos::new(0, 1, 0), &world));
}
#[test]
fn test_is_standable() {
let mut partial_world = PartialInstance::default();
let mut chunk_storage = ChunkStorage::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 0, 0),
azalea_registry::Block::Stone.into(),
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 1, 0),
BlockState::AIR,
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 2, 0),
BlockState::AIR,
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 3, 0),
BlockState::AIR,
&chunk_storage,
);
let world = chunk_storage.into();
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));
}
}

View file

@ -0,0 +1,209 @@
use azalea_client::{SprintDirection, StartSprintEvent};
use azalea_core::{BlockPos, CardinalDirection};
use azalea_world::Instance;
use crate::{pathfinder::astar, JumpEvent, LookAtEvent};
use super::{
fall_distance, is_block_passable, is_passable, is_standable, Edge, ExecuteCtx, MoveData,
FALL_ONE_BLOCK_COST, JUMP_COST, WALK_ONE_BLOCK_COST,
};
pub fn basic_move(world: &Instance, 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
}
fn forward_move(world: &Instance, 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) {
continue;
}
let cost = WALK_ONE_BLOCK_COST;
edges.push(Edge {
movement: astar::Movement {
target: pos + offset,
data: MoveData {
execute: &execute_forward_move,
},
},
cost,
})
}
edges
}
fn execute_forward_move(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
fn ascend_move(world: &Instance, 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) || !is_standable(&(pos + offset), world) {
continue;
}
let cost = WALK_ONE_BLOCK_COST + JUMP_COST;
edges.push(Edge {
movement: astar::Movement {
target: pos + offset,
data: MoveData {
execute: &execute_ascend_move,
},
},
cost,
})
}
edges
}
fn execute_ascend_move(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
jump_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
jump_events.send(JumpEvent { entity });
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
fn descend_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
let mut edges = Vec::new();
for dir in CardinalDirection::iter() {
let new_horizontal_position = pos + BlockPos::new(dir.x(), 0, dir.z());
let fall_distance = fall_distance(&new_horizontal_position, world);
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) {
continue;
}
let cost = WALK_ONE_BLOCK_COST + FALL_ONE_BLOCK_COST * fall_distance as f32;
edges.push(Edge {
movement: astar::Movement {
target: new_position,
data: MoveData {
execute: &execute_descend_move,
},
},
cost,
})
}
edges
}
fn execute_descend_move(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}
fn diagonal_move(world: &Instance, 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,
) {
continue;
}
if !is_standable(&(pos + offset), world) {
continue;
}
let cost = WALK_ONE_BLOCK_COST * 1.4;
edges.push(Edge {
movement: astar::Movement {
target: pos + offset,
data: MoveData {
execute: &execute_diagonal_move,
},
},
cost,
})
}
edges
}
fn execute_diagonal_move(
ExecuteCtx {
entity,
target,
look_at_events,
sprint_events,
..
}: ExecuteCtx,
) {
let center = target.center();
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
}

View file

@ -0,0 +1,182 @@
pub mod basic;
use crate::{JumpEvent, LookAtEvent};
use super::astar;
use azalea_client::{StartSprintEvent, StartWalkEvent};
use azalea_core::{BlockPos, Vec3};
use azalea_physics::collision::{self, BlockWithShape};
use azalea_world::Instance;
use bevy_ecs::{entity::Entity, event::EventWriter};
type Edge = astar::Edge<BlockPos, MoveData>;
#[derive(Clone)]
pub struct MoveData {
// pub move_kind: BasicMoves,
pub execute: &'static (dyn Fn(ExecuteCtx) + Send + Sync),
}
/// whether this block is passable
fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
if let Some(block) = world.chunks.get_block_state(pos) {
if block.shape() != &collision::empty_shape() {
return false;
}
if block == azalea_registry::Block::Water.into() {
return false;
}
if block.waterlogged() {
return false;
}
block.shape() == &collision::empty_shape()
} else {
false
}
}
/// whether this block has a solid hitbox (i.e. we can stand on it)
fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
if let Some(block) = world.chunks.get_block_state(pos) {
block.shape() == &collision::block_shape()
} else {
false
}
}
/// Whether this block and the block above are passable
fn is_passable(pos: &BlockPos, world: &Instance) -> bool {
is_block_passable(pos, world) && is_block_passable(&pos.up(1), world)
}
/// Whether we can stand in this position. Checks if the block below is solid,
/// and that the two blocks above that are passable.
fn is_standable(pos: &BlockPos, world: &Instance) -> bool {
is_block_solid(&pos.down(1), world) && is_passable(pos, world)
}
/// Get the amount of air blocks until the next solid block below this one.
fn fall_distance(pos: &BlockPos, world: &Instance) -> 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);
if current_pos.y < world.chunks.min_y {
return u32::MAX;
}
}
distance
}
const JUMP_COST: f32 = 0.5;
const WALK_ONE_BLOCK_COST: f32 = 1.0;
const FALL_ONE_BLOCK_COST: f32 = 0.5;
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
pub entity: Entity,
pub target: BlockPos,
pub position: Vec3,
pub look_at_events: &'a mut EventWriter<'w1, LookAtEvent>,
pub sprint_events: &'a mut EventWriter<'w2, StartSprintEvent>,
pub walk_events: &'a mut EventWriter<'w3, StartWalkEvent>,
pub jump_events: &'a mut EventWriter<'w4, JumpEvent>,
}
#[cfg(test)]
mod tests {
use super::*;
use azalea_block::BlockState;
use azalea_core::ChunkPos;
use azalea_world::{Chunk, ChunkStorage, PartialInstance};
#[test]
fn test_is_passable() {
let mut partial_world = PartialInstance::default();
let mut chunk_storage = ChunkStorage::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 0, 0),
azalea_registry::Block::Stone.into(),
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 1, 0),
BlockState::AIR,
&chunk_storage,
);
let world = chunk_storage.into();
assert!(!is_block_passable(&BlockPos::new(0, 0, 0), &world));
assert!(is_block_passable(&BlockPos::new(0, 1, 0), &world));
}
#[test]
fn test_is_solid() {
let mut partial_world = PartialInstance::default();
let mut chunk_storage = ChunkStorage::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 0, 0),
azalea_registry::Block::Stone.into(),
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 1, 0),
BlockState::AIR,
&chunk_storage,
);
let world = chunk_storage.into();
assert!(is_block_solid(&BlockPos::new(0, 0, 0), &world));
assert!(!is_block_solid(&BlockPos::new(0, 1, 0), &world));
}
#[test]
fn test_is_standable() {
let mut partial_world = PartialInstance::default();
let mut chunk_storage = ChunkStorage::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 0, 0),
azalea_registry::Block::Stone.into(),
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 1, 0),
BlockState::AIR,
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 2, 0),
BlockState::AIR,
&chunk_storage,
);
partial_world.chunks.set_block_state(
&BlockPos::new(0, 3, 0),
BlockState::AIR,
&chunk_storage,
);
let world = chunk_storage.into();
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));
}
}