mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
PathfinderCtx
This commit is contained in:
parent
f10535b5c8
commit
0297b8aace
6 changed files with 452 additions and 437 deletions
|
@ -4,7 +4,7 @@ use azalea::{
|
|||
pathfinder::{
|
||||
astar::{self, a_star},
|
||||
goals::BlockPosGoal,
|
||||
moves::PathfinderCtx,
|
||||
world::CachedWorld,
|
||||
Goal,
|
||||
},
|
||||
BlockPos,
|
||||
|
@ -79,13 +79,11 @@ fn bench_pathfinder(c: &mut Criterion) {
|
|||
|
||||
b.iter(|| {
|
||||
let (world, start, end) = generate_bedrock_world(&mut partial_chunks, 4);
|
||||
let ctx = PathfinderCtx::new(Arc::new(RwLock::new(world.into())));
|
||||
let cached_world = CachedWorld::new(Arc::new(RwLock::new(world.into())));
|
||||
let goal = BlockPosGoal(end);
|
||||
|
||||
let successors = |pos: BlockPos| {
|
||||
let mut edges = Vec::with_capacity(16);
|
||||
successors_fn(&mut edges, &ctx, pos);
|
||||
edges
|
||||
azalea::pathfinder::call_successors_fn(&cached_world, successors_fn, pos)
|
||||
};
|
||||
|
||||
let astar::Path { movements, partial } = a_star(
|
||||
|
|
|
@ -6,6 +6,7 @@ pub mod costs;
|
|||
pub mod goals;
|
||||
pub mod moves;
|
||||
pub mod simulation;
|
||||
pub mod world;
|
||||
|
||||
use crate::bot::{JumpEvent, LookAtEvent};
|
||||
use crate::pathfinder::astar::a_star;
|
||||
|
@ -20,6 +21,7 @@ use crate::ecs::{
|
|||
system::{Commands, Query, Res},
|
||||
};
|
||||
use crate::pathfinder::moves::PathfinderCtx;
|
||||
use crate::pathfinder::world::CachedWorld;
|
||||
use azalea_client::chat::SendChatEvent;
|
||||
use azalea_client::movement::walk_listener;
|
||||
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
||||
|
@ -219,12 +221,8 @@ fn goto_listener(
|
|||
let task = thread_pool.spawn(async move {
|
||||
debug!("start: {start:?}");
|
||||
|
||||
let ctx = PathfinderCtx::new(world_lock);
|
||||
let successors = |pos: BlockPos| {
|
||||
let mut edges = Vec::with_capacity(16);
|
||||
successors_fn(&mut edges, &ctx, pos);
|
||||
edges
|
||||
};
|
||||
let cached_world = CachedWorld::new(world_lock);
|
||||
let successors = |pos: BlockPos| call_successors_fn(&cached_world, successors_fn, pos);
|
||||
|
||||
let mut attempt_number = 0;
|
||||
|
||||
|
@ -332,12 +330,9 @@ fn path_found_listener(
|
|||
.get(instance_name)
|
||||
.expect("Entity tried to pathfind but the entity isn't in a valid world");
|
||||
let successors_fn: moves::SuccessorsFn = event.successors_fn;
|
||||
let ctx = PathfinderCtx::new(world_lock);
|
||||
let successors = |pos: BlockPos| {
|
||||
let mut edges = Vec::with_capacity(16);
|
||||
successors_fn(&mut edges, &ctx, pos);
|
||||
edges
|
||||
};
|
||||
let cached_world = CachedWorld::new(world_lock);
|
||||
let successors =
|
||||
|pos: BlockPos| call_successors_fn(&cached_world, successors_fn, pos);
|
||||
|
||||
if let Some(first_node_of_new_path) = path.front() {
|
||||
if successors(last_node_of_current_path.target)
|
||||
|
@ -521,12 +516,8 @@ fn check_for_path_obstruction(
|
|||
.expect("Entity tried to pathfind but the entity isn't in a valid world");
|
||||
|
||||
// obstruction check (the path we're executing isn't possible anymore)
|
||||
let ctx = PathfinderCtx::new(world_lock);
|
||||
let successors = |pos: BlockPos| {
|
||||
let mut edges = Vec::with_capacity(16);
|
||||
successors_fn(&mut edges, &ctx, pos);
|
||||
edges
|
||||
};
|
||||
let cached_world = CachedWorld::new(world_lock);
|
||||
let successors = |pos: BlockPos| call_successors_fn(&cached_world, successors_fn, pos);
|
||||
|
||||
if let Some(obstructed_index) = check_path_obstructed(
|
||||
executing_path.last_reached_node,
|
||||
|
@ -816,6 +807,20 @@ where
|
|||
None
|
||||
}
|
||||
|
||||
pub fn call_successors_fn(
|
||||
cached_world: &CachedWorld,
|
||||
successors_fn: SuccessorsFn,
|
||||
pos: BlockPos,
|
||||
) -> Vec<astar::Edge<BlockPos, moves::MoveData>> {
|
||||
let mut edges = Vec::with_capacity(16);
|
||||
let mut ctx = PathfinderCtx {
|
||||
edges: &mut edges,
|
||||
world: cached_world,
|
||||
};
|
||||
successors_fn(&mut ctx, pos);
|
||||
edges
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
|
|
@ -10,25 +10,25 @@ use crate::pathfinder::{astar, costs::*};
|
|||
|
||||
use super::{default_is_reached, Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
|
||||
|
||||
pub fn basic_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, node: BlockPos) {
|
||||
forward_move(edges, ctx, node);
|
||||
ascend_move(edges, ctx, node);
|
||||
descend_move(edges, ctx, node);
|
||||
diagonal_move(edges, ctx, node);
|
||||
descend_forward_1_move(edges, ctx, node);
|
||||
pub fn basic_move(ctx: &mut PathfinderCtx, node: BlockPos) {
|
||||
forward_move(ctx, node);
|
||||
ascend_move(ctx, node);
|
||||
descend_move(ctx, node);
|
||||
diagonal_move(ctx, node);
|
||||
descend_forward_1_move(ctx, node);
|
||||
}
|
||||
|
||||
fn forward_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn forward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
for dir in CardinalDirection::iter() {
|
||||
let offset = BlockPos::new(dir.x(), 0, dir.z());
|
||||
|
||||
if !ctx.is_standable(pos + offset) {
|
||||
if !ctx.world.is_standable(pos + offset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = SPRINT_ONE_BLOCK_COST;
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos + offset,
|
||||
data: MoveData {
|
||||
|
@ -47,20 +47,20 @@ fn execute_forward_move(mut ctx: ExecuteCtx) {
|
|||
ctx.sprint(SprintDirection::Forward);
|
||||
}
|
||||
|
||||
fn ascend_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn ascend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
for dir in CardinalDirection::iter() {
|
||||
let offset = BlockPos::new(dir.x(), 1, dir.z());
|
||||
|
||||
if !ctx.is_block_passable(pos.up(2)) {
|
||||
if !ctx.world.is_block_passable(pos.up(2)) {
|
||||
continue;
|
||||
}
|
||||
if !ctx.is_standable(pos + offset) {
|
||||
if !ctx.world.is_standable(pos + offset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = SPRINT_ONE_BLOCK_COST + JUMP_PENALTY + *JUMP_ONE_BLOCK_COST;
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos + offset,
|
||||
data: MoveData {
|
||||
|
@ -119,22 +119,22 @@ pub fn ascend_is_reached(
|
|||
BlockPos::from(position) == target || BlockPos::from(position) == target.down(1)
|
||||
}
|
||||
|
||||
fn descend_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn descend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
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 = ctx.fall_distance(new_horizontal_position);
|
||||
let fall_distance = ctx.world.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 !ctx.is_passable(new_horizontal_position) {
|
||||
if !ctx.world.is_passable(new_horizontal_position) {
|
||||
continue;
|
||||
}
|
||||
// check whether we can stand on the target position
|
||||
if !ctx.is_standable(new_position) {
|
||||
if !ctx.world.is_standable(new_position) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ fn descend_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
|||
CENTER_AFTER_FALL_COST,
|
||||
);
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: new_position,
|
||||
data: MoveData {
|
||||
|
@ -214,14 +214,14 @@ pub fn descend_is_reached(
|
|||
&& (position.y - target.y as f64) < 0.5
|
||||
}
|
||||
|
||||
fn descend_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn descend_forward_1_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
for dir in CardinalDirection::iter() {
|
||||
let dir_delta = BlockPos::new(dir.x(), 0, dir.z());
|
||||
let gap_horizontal_position = pos + dir_delta;
|
||||
let new_horizontal_position = pos + dir_delta * 2;
|
||||
|
||||
let gap_fall_distance = ctx.fall_distance(gap_horizontal_position);
|
||||
let fall_distance = ctx.fall_distance(new_horizontal_position);
|
||||
let gap_fall_distance = ctx.world.fall_distance(gap_horizontal_position);
|
||||
let fall_distance = ctx.world.fall_distance(new_horizontal_position);
|
||||
|
||||
if fall_distance == 0 || fall_distance > 3 || gap_fall_distance < fall_distance {
|
||||
continue;
|
||||
|
@ -230,14 +230,14 @@ fn descend_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
let new_position = new_horizontal_position.down(fall_distance as i32);
|
||||
|
||||
// check whether 2 blocks vertically forward are passable
|
||||
if !ctx.is_passable(new_horizontal_position) {
|
||||
if !ctx.world.is_passable(new_horizontal_position) {
|
||||
continue;
|
||||
}
|
||||
if !ctx.is_passable(gap_horizontal_position) {
|
||||
if !ctx.world.is_passable(gap_horizontal_position) {
|
||||
continue;
|
||||
}
|
||||
// check whether we can stand on the target position
|
||||
if !ctx.is_standable(new_position) {
|
||||
if !ctx.world.is_standable(new_position) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -253,7 +253,7 @@ fn descend_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
CENTER_AFTER_FALL_COST,
|
||||
);
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: new_position,
|
||||
data: MoveData {
|
||||
|
@ -266,7 +266,7 @@ fn descend_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
}
|
||||
}
|
||||
|
||||
fn diagonal_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn diagonal_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
for dir in CardinalDirection::iter() {
|
||||
let right = dir.right();
|
||||
let offset = BlockPos::new(dir.x() + right.x(), 0, dir.z() + right.z());
|
||||
|
@ -276,8 +276,8 @@ fn diagonal_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
|||
// +0.001 so it doesn't unnecessarily go diagonal sometimes
|
||||
let mut cost = SPRINT_ONE_BLOCK_COST * SQRT_2 + 0.001;
|
||||
|
||||
let left_passable = ctx.is_passable(left_pos);
|
||||
let right_passable = ctx.is_passable(right_pos);
|
||||
let left_passable = ctx.world.is_passable(left_pos);
|
||||
let right_passable = ctx.world.is_passable(right_pos);
|
||||
|
||||
if !left_passable && !right_passable {
|
||||
continue;
|
||||
|
@ -288,11 +288,11 @@ fn diagonal_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
|||
cost += WALK_ONE_BLOCK_COST / 2.;
|
||||
}
|
||||
|
||||
if !ctx.is_standable(pos + offset) {
|
||||
if !ctx.world.is_standable(pos + offset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos + offset,
|
||||
data: MoveData {
|
||||
|
|
|
@ -1,33 +1,22 @@
|
|||
pub mod basic;
|
||||
pub mod parkour;
|
||||
|
||||
use std::{
|
||||
cell::{RefCell, UnsafeCell},
|
||||
fmt::Debug,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{JumpEvent, LookAtEvent};
|
||||
|
||||
use super::astar;
|
||||
use azalea_block::BlockState;
|
||||
use super::{astar, world::CachedWorld};
|
||||
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
|
||||
use azalea_core::{
|
||||
bitset::FixedBitSet,
|
||||
position::{BlockPos, ChunkPos, ChunkSectionBlockPos, ChunkSectionPos, Vec3},
|
||||
};
|
||||
use azalea_physics::collision::BlockWithShape;
|
||||
use azalea_world::Instance;
|
||||
use azalea_core::position::{BlockPos, Vec3};
|
||||
use bevy_ecs::{entity::Entity, event::EventWriter};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
type Edge = astar::Edge<BlockPos, MoveData>;
|
||||
|
||||
pub type SuccessorsFn = fn(&mut Vec<Edge>, &PathfinderCtx, BlockPos);
|
||||
pub type SuccessorsFn = fn(&mut PathfinderCtx, BlockPos);
|
||||
|
||||
pub fn default_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, node: BlockPos) {
|
||||
basic::basic_move(edges, ctx, node);
|
||||
parkour::parkour_move(edges, ctx, node);
|
||||
pub fn default_move(ctx: &mut PathfinderCtx, node: BlockPos) {
|
||||
basic::basic_move(ctx, node);
|
||||
parkour::parkour_move(ctx, node);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -46,262 +35,6 @@ impl Debug for MoveData {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PathfinderCtx {
|
||||
min_y: i32,
|
||||
world_lock: Arc<RwLock<Instance>>,
|
||||
// we store `PalettedContainer`s instead of `Chunk`s or `Section`s because it doesn't contain
|
||||
// any unnecessary data like heightmaps or biomes.
|
||||
cached_chunks: RefCell<Vec<(ChunkPos, Vec<azalea_world::palette::PalettedContainer>)>>,
|
||||
|
||||
cached_blocks: UnsafeCell<CachedSections>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CachedSections {
|
||||
pub last_index: usize,
|
||||
pub second_last_index: usize,
|
||||
pub sections: Vec<CachedSection>,
|
||||
}
|
||||
|
||||
impl CachedSections {
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, pos: ChunkSectionPos) -> Option<&mut CachedSection> {
|
||||
if let Some(last_item) = self.sections.get(self.last_index) {
|
||||
if last_item.pos == pos {
|
||||
return Some(&mut self.sections[self.last_index]);
|
||||
} else if let Some(second_last_item) = self.sections.get(self.second_last_index) {
|
||||
if second_last_item.pos == pos {
|
||||
return Some(&mut self.sections[self.second_last_index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let index = self
|
||||
.sections
|
||||
.binary_search_by(|section| section.pos.cmp(&pos))
|
||||
.ok();
|
||||
|
||||
if let Some(index) = index {
|
||||
self.second_last_index = self.last_index;
|
||||
self.last_index = index;
|
||||
return Some(&mut self.sections[index]);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn insert(&mut self, section: CachedSection) {
|
||||
// self.sections.push(section);
|
||||
// self.sections.sort_unstable_by(|a, b| a.pos.cmp(&b.pos));
|
||||
let index = self
|
||||
.sections
|
||||
.binary_search_by(|s| s.pos.cmp(§ion.pos))
|
||||
.unwrap_or_else(|e| e);
|
||||
self.sections.insert(index, section);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CachedSection {
|
||||
pub pos: ChunkSectionPos,
|
||||
pub passable_bitset: FixedBitSet<4096>,
|
||||
pub solid_bitset: FixedBitSet<4096>,
|
||||
}
|
||||
|
||||
impl PathfinderCtx {
|
||||
pub fn new(world_lock: Arc<RwLock<Instance>>) -> Self {
|
||||
let min_y = world_lock.read().chunks.min_y;
|
||||
Self {
|
||||
min_y,
|
||||
world_lock,
|
||||
cached_chunks: Default::default(),
|
||||
cached_blocks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// ```
|
||||
// fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
|
||||
// self.with_section(ChunkSectionPos::from(pos), |section| {
|
||||
// let state = section.get(pos.x as usize, pos.y as usize, pos.z as usize);
|
||||
// BlockState::try_from(state).unwrap_or(BlockState::AIR)
|
||||
// })
|
||||
// }
|
||||
// ```
|
||||
|
||||
fn with_section<T>(
|
||||
&self,
|
||||
section_pos: ChunkSectionPos,
|
||||
f: impl FnOnce(&azalea_world::palette::PalettedContainer) -> T,
|
||||
) -> Option<T> {
|
||||
let chunk_pos = ChunkPos::from(section_pos);
|
||||
let section_index =
|
||||
azalea_world::chunk_storage::section_index(section_pos.y * 16, self.min_y) as usize;
|
||||
|
||||
let mut cached_chunks = self.cached_chunks.borrow_mut();
|
||||
|
||||
// get section from cache
|
||||
if let Some(sections) = cached_chunks.iter().find_map(|(pos, sections)| {
|
||||
if *pos == chunk_pos {
|
||||
Some(sections)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if section_index >= sections.len() {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
};
|
||||
let section: &azalea_world::palette::PalettedContainer = §ions[section_index];
|
||||
return Some(f(section));
|
||||
}
|
||||
|
||||
let world = self.world_lock.read();
|
||||
let Some(chunk) = world.chunks.get(&chunk_pos) else {
|
||||
return None;
|
||||
};
|
||||
let chunk = chunk.read();
|
||||
|
||||
let sections: Vec<azalea_world::palette::PalettedContainer> = chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| section.states.clone())
|
||||
.collect();
|
||||
|
||||
if section_index >= sections.len() {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
};
|
||||
|
||||
let section = §ions[section_index];
|
||||
let r = f(section);
|
||||
|
||||
// add the sections to the chunk cache
|
||||
cached_chunks.push((chunk_pos, sections));
|
||||
|
||||
Some(r)
|
||||
}
|
||||
|
||||
fn calculate_bitsets_for_section(&self, section_pos: ChunkSectionPos) -> Option<CachedSection> {
|
||||
self.with_section(section_pos, |section| {
|
||||
let mut passable_bitset = FixedBitSet::<4096>::new();
|
||||
let mut solid_bitset = FixedBitSet::<4096>::new();
|
||||
for i in 0..4096 {
|
||||
let block_state_id = section.get_at_index(i);
|
||||
let block_state = BlockState::try_from(block_state_id).unwrap_or(BlockState::AIR);
|
||||
if is_block_state_passable(block_state) {
|
||||
passable_bitset.set(i);
|
||||
}
|
||||
if is_block_state_solid(block_state) {
|
||||
solid_bitset.set(i);
|
||||
}
|
||||
}
|
||||
CachedSection {
|
||||
pos: section_pos,
|
||||
passable_bitset,
|
||||
solid_bitset,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_block_passable(&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.passable_bitset.index(index);
|
||||
}
|
||||
|
||||
let Some(cached) = self.calculate_bitsets_for_section(section_pos) else {
|
||||
return false;
|
||||
};
|
||||
let passable = cached.passable_bitset.index(index);
|
||||
cached_blocks.insert(cached);
|
||||
passable
|
||||
}
|
||||
|
||||
pub fn is_block_solid(&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.solid_bitset.index(index);
|
||||
}
|
||||
|
||||
let Some(cached) = self.calculate_bitsets_for_section(section_pos) else {
|
||||
return false;
|
||||
};
|
||||
let solid = cached.solid_bitset.index(index);
|
||||
cached_blocks.insert(cached);
|
||||
solid
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Whether we can stand in this position. Checks if the block below is
|
||||
/// solid, and that the two blocks above that are passable.
|
||||
|
||||
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 < self.min_y {
|
||||
return u32::MAX;
|
||||
}
|
||||
}
|
||||
distance
|
||||
}
|
||||
}
|
||||
|
||||
/// whether this block is passable
|
||||
fn is_block_state_passable(block: BlockState) -> bool {
|
||||
if block.is_air() {
|
||||
// fast path
|
||||
return true;
|
||||
}
|
||||
if !block.is_shape_empty() {
|
||||
return false;
|
||||
}
|
||||
if block == azalea_registry::Block::Water.into() {
|
||||
return false;
|
||||
}
|
||||
if block.waterlogged() {
|
||||
return false;
|
||||
}
|
||||
if block == azalea_registry::Block::Lava.into() {
|
||||
return false;
|
||||
}
|
||||
// block.waterlogged currently doesn't account for seagrass and some other water
|
||||
// blocks
|
||||
if block == azalea_registry::Block::Seagrass.into() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
||||
fn is_block_state_solid(block: BlockState) -> bool {
|
||||
if block.is_air() {
|
||||
// fast path
|
||||
return false;
|
||||
}
|
||||
block.is_shape_full()
|
||||
}
|
||||
|
||||
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
|
||||
pub entity: Entity,
|
||||
/// The node that we're trying to reach.
|
||||
|
@ -371,81 +104,7 @@ pub fn default_is_reached(
|
|||
BlockPos::from(position) == target
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use azalea_block::BlockState;
|
||||
use azalea_core::position::ChunkPos;
|
||||
use azalea_world::{Chunk, ChunkStorage, PartialInstance};
|
||||
|
||||
#[test]
|
||||
fn test_is_passable() {
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut world = ChunkStorage::default();
|
||||
|
||||
partial_world
|
||||
.chunks
|
||||
.set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
|
||||
partial_world.chunks.set_block_state(
|
||||
&BlockPos::new(0, 0, 0),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
&world,
|
||||
);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
|
||||
|
||||
let ctx = PathfinderCtx::new(Arc::new(RwLock::new(world.into())));
|
||||
assert!(!ctx.is_block_passable(BlockPos::new(0, 0, 0)));
|
||||
assert!(ctx.is_block_passable(BlockPos::new(0, 1, 0),));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_solid() {
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut world = ChunkStorage::default();
|
||||
partial_world
|
||||
.chunks
|
||||
.set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
|
||||
partial_world.chunks.set_block_state(
|
||||
&BlockPos::new(0, 0, 0),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
&world,
|
||||
);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
|
||||
|
||||
let ctx = PathfinderCtx::new(Arc::new(RwLock::new(world.into())));
|
||||
assert!(ctx.is_block_solid(BlockPos::new(0, 0, 0)));
|
||||
assert!(!ctx.is_block_solid(BlockPos::new(0, 1, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_standable() {
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut world = ChunkStorage::default();
|
||||
partial_world
|
||||
.chunks
|
||||
.set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
|
||||
partial_world.chunks.set_block_state(
|
||||
&BlockPos::new(0, 0, 0),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
&world,
|
||||
);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 2, 0), BlockState::AIR, &world);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 3, 0), BlockState::AIR, &world);
|
||||
|
||||
let ctx = PathfinderCtx::new(Arc::new(RwLock::new(world.into())));
|
||||
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)));
|
||||
}
|
||||
pub struct PathfinderCtx<'a> {
|
||||
pub edges: &'a mut Vec<Edge>,
|
||||
pub world: &'a CachedWorld,
|
||||
}
|
||||
|
|
|
@ -5,29 +5,29 @@ use crate::pathfinder::{astar, costs::*};
|
|||
|
||||
use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
|
||||
|
||||
pub fn parkour_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, node: BlockPos) {
|
||||
parkour_forward_1_move(edges, ctx, node);
|
||||
parkour_forward_2_move(edges, ctx, node);
|
||||
parkour_forward_3_move(edges, ctx, node);
|
||||
pub fn parkour_move(ctx: &mut PathfinderCtx, node: BlockPos) {
|
||||
parkour_forward_1_move(ctx, node);
|
||||
parkour_forward_2_move(ctx, node);
|
||||
parkour_forward_3_move(ctx, node);
|
||||
}
|
||||
|
||||
fn parkour_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn parkour_forward_1_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
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);
|
||||
|
||||
// make sure we actually have to jump
|
||||
if ctx.is_block_solid((pos + gap_offset).down(1)) {
|
||||
if ctx.world.is_block_solid((pos + gap_offset).down(1)) {
|
||||
continue;
|
||||
}
|
||||
if !ctx.is_passable(pos + gap_offset) {
|
||||
if !ctx.world.is_passable(pos + gap_offset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ascend: i32 = if ctx.is_standable(pos + offset.up(1)) {
|
||||
let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
|
||||
// ascend
|
||||
1
|
||||
} else if ctx.is_standable(pos + offset) {
|
||||
} else if ctx.world.is_standable(pos + offset) {
|
||||
// forward
|
||||
0
|
||||
} else {
|
||||
|
@ -35,22 +35,22 @@ fn parkour_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
};
|
||||
|
||||
// make sure we have space to jump
|
||||
if !ctx.is_block_passable((pos + gap_offset).up(2)) {
|
||||
if !ctx.world.is_block_passable((pos + gap_offset).up(2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// make sure there's not a block above us
|
||||
if !ctx.is_block_passable(pos.up(2)) {
|
||||
if !ctx.world.is_block_passable(pos.up(2)) {
|
||||
continue;
|
||||
}
|
||||
// make sure there's not a block above the target
|
||||
if !ctx.is_block_passable((pos + offset).up(2)) {
|
||||
if !ctx.world.is_block_passable((pos + offset).up(2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 2. + CENTER_AFTER_FALL_COST;
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos + offset.up(ascend),
|
||||
data: MoveData {
|
||||
|
@ -63,22 +63,22 @@ fn parkour_forward_1_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
}
|
||||
}
|
||||
|
||||
fn parkour_forward_2_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn parkour_forward_2_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
'dir: 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);
|
||||
|
||||
// make sure we actually have to jump
|
||||
if ctx.is_block_solid((pos + gap_1_offset).down(1))
|
||||
|| ctx.is_block_solid((pos + gap_2_offset).down(1))
|
||||
if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
|
||||
|| ctx.world.is_block_solid((pos + gap_2_offset).down(1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let ascend: i32 = if ctx.is_standable(pos + offset.up(1)) {
|
||||
let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
|
||||
1
|
||||
} else if ctx.is_standable(pos + offset) {
|
||||
} else if ctx.world.is_standable(pos + offset) {
|
||||
0
|
||||
} else {
|
||||
continue;
|
||||
|
@ -86,25 +86,25 @@ fn parkour_forward_2_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
|
||||
// make sure we have space to jump
|
||||
for offset in [gap_1_offset, gap_2_offset] {
|
||||
if !ctx.is_passable(pos + offset) {
|
||||
if !ctx.world.is_passable(pos + offset) {
|
||||
continue 'dir;
|
||||
}
|
||||
if !ctx.is_block_passable((pos + offset).up(2)) {
|
||||
if !ctx.world.is_block_passable((pos + offset).up(2)) {
|
||||
continue 'dir;
|
||||
}
|
||||
}
|
||||
// make sure there's not a block above us
|
||||
if !ctx.is_block_passable(pos.up(2)) {
|
||||
if !ctx.world.is_block_passable(pos.up(2)) {
|
||||
continue;
|
||||
}
|
||||
// make sure there's not a block above the target
|
||||
if !ctx.is_block_passable((pos + offset).up(2)) {
|
||||
if !ctx.world.is_block_passable((pos + offset).up(2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 3. + CENTER_AFTER_FALL_COST;
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos + offset.up(ascend),
|
||||
data: MoveData {
|
||||
|
@ -117,7 +117,7 @@ fn parkour_forward_2_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
}
|
||||
}
|
||||
|
||||
fn parkour_forward_3_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: BlockPos) {
|
||||
fn parkour_forward_3_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
'dir: 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);
|
||||
|
@ -125,38 +125,38 @@ fn parkour_forward_3_move(edges: &mut Vec<Edge>, ctx: &PathfinderCtx, pos: Block
|
|||
let offset = BlockPos::new(dir.x() * 4, 0, dir.z() * 4);
|
||||
|
||||
// make sure we actually have to jump
|
||||
if ctx.is_block_solid((pos + gap_1_offset).down(1))
|
||||
|| ctx.is_block_solid((pos + gap_2_offset).down(1))
|
||||
|| ctx.is_block_solid((pos + gap_3_offset).down(1))
|
||||
if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
|
||||
|| ctx.world.is_block_solid((pos + gap_2_offset).down(1))
|
||||
|| ctx.world.is_block_solid((pos + gap_3_offset).down(1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if !ctx.is_standable(pos + offset) {
|
||||
if !ctx.world.is_standable(pos + offset) {
|
||||
continue;
|
||||
};
|
||||
|
||||
// make sure we have space to jump
|
||||
for offset in [gap_1_offset, gap_2_offset, gap_3_offset] {
|
||||
if !ctx.is_passable(pos + offset) {
|
||||
if !ctx.world.is_passable(pos + offset) {
|
||||
continue 'dir;
|
||||
}
|
||||
if !ctx.is_block_passable((pos + offset).up(2)) {
|
||||
if !ctx.world.is_block_passable((pos + offset).up(2)) {
|
||||
continue 'dir;
|
||||
}
|
||||
}
|
||||
// make sure there's not a block above us
|
||||
if !ctx.is_block_passable(pos.up(2)) {
|
||||
if !ctx.world.is_block_passable(pos.up(2)) {
|
||||
continue;
|
||||
}
|
||||
// make sure there's not a block above the target
|
||||
if !ctx.is_block_passable((pos + offset).up(2)) {
|
||||
if !ctx.world.is_block_passable((pos + offset).up(2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = JUMP_PENALTY + SPRINT_ONE_BLOCK_COST * 4. + CENTER_AFTER_FALL_COST;
|
||||
|
||||
edges.push(Edge {
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos + offset,
|
||||
data: MoveData {
|
||||
|
|
353
azalea/src/pathfinder/world.rs
Normal file
353
azalea/src/pathfinder/world.rs
Normal file
|
@ -0,0 +1,353 @@
|
|||
use std::{
|
||||
cell::{RefCell, UnsafeCell},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use azalea_block::BlockState;
|
||||
use azalea_core::{
|
||||
bitset::FixedBitSet,
|
||||
position::{BlockPos, ChunkPos, ChunkSectionBlockPos, ChunkSectionPos},
|
||||
};
|
||||
use azalea_physics::collision::BlockWithShape;
|
||||
use azalea_world::Instance;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
/// An efficient representation of the world used for the pathfinder.
|
||||
pub struct CachedWorld {
|
||||
min_y: i32,
|
||||
world_lock: Arc<RwLock<Instance>>,
|
||||
|
||||
// we store `PalettedContainer`s instead of `Chunk`s or `Section`s because it doesn't contain
|
||||
// any unnecessary data like heightmaps or biomes.
|
||||
cached_chunks: RefCell<Vec<(ChunkPos, Vec<azalea_world::palette::PalettedContainer>)>>,
|
||||
|
||||
cached_blocks: UnsafeCell<CachedSections>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CachedSections {
|
||||
pub last_index: usize,
|
||||
pub second_last_index: usize,
|
||||
pub sections: Vec<CachedSection>,
|
||||
}
|
||||
|
||||
impl CachedSections {
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, pos: ChunkSectionPos) -> Option<&mut CachedSection> {
|
||||
if let Some(last_item) = self.sections.get(self.last_index) {
|
||||
if last_item.pos == pos {
|
||||
return Some(&mut self.sections[self.last_index]);
|
||||
} else if let Some(second_last_item) = self.sections.get(self.second_last_index) {
|
||||
if second_last_item.pos == pos {
|
||||
return Some(&mut self.sections[self.second_last_index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let index = self
|
||||
.sections
|
||||
.binary_search_by(|section| section.pos.cmp(&pos))
|
||||
.ok();
|
||||
|
||||
if let Some(index) = index {
|
||||
self.second_last_index = self.last_index;
|
||||
self.last_index = index;
|
||||
return Some(&mut self.sections[index]);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn insert(&mut self, section: CachedSection) {
|
||||
// self.sections.push(section);
|
||||
// self.sections.sort_unstable_by(|a, b| a.pos.cmp(&b.pos));
|
||||
let index = self
|
||||
.sections
|
||||
.binary_search_by(|s| s.pos.cmp(§ion.pos))
|
||||
.unwrap_or_else(|e| e);
|
||||
self.sections.insert(index, section);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CachedSection {
|
||||
pub pos: ChunkSectionPos,
|
||||
pub passable_bitset: FixedBitSet<4096>,
|
||||
pub solid_bitset: FixedBitSet<4096>,
|
||||
}
|
||||
|
||||
impl CachedWorld {
|
||||
pub fn new(world_lock: Arc<RwLock<Instance>>) -> Self {
|
||||
let min_y = world_lock.read().chunks.min_y;
|
||||
Self {
|
||||
min_y,
|
||||
world_lock,
|
||||
cached_chunks: Default::default(),
|
||||
cached_blocks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// ```
|
||||
// fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
|
||||
// self.with_section(ChunkSectionPos::from(pos), |section| {
|
||||
// let state = section.get(pos.x as usize, pos.y as usize, pos.z as usize);
|
||||
// BlockState::try_from(state).unwrap_or(BlockState::AIR)
|
||||
// })
|
||||
// }
|
||||
// ```
|
||||
|
||||
fn with_section<T>(
|
||||
&self,
|
||||
section_pos: ChunkSectionPos,
|
||||
f: impl FnOnce(&azalea_world::palette::PalettedContainer) -> T,
|
||||
) -> Option<T> {
|
||||
let chunk_pos = ChunkPos::from(section_pos);
|
||||
let section_index =
|
||||
azalea_world::chunk_storage::section_index(section_pos.y * 16, self.min_y) as usize;
|
||||
|
||||
let mut cached_chunks = self.cached_chunks.borrow_mut();
|
||||
|
||||
// get section from cache
|
||||
if let Some(sections) = cached_chunks.iter().find_map(|(pos, sections)| {
|
||||
if *pos == chunk_pos {
|
||||
Some(sections)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if section_index >= sections.len() {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
};
|
||||
let section: &azalea_world::palette::PalettedContainer = §ions[section_index];
|
||||
return Some(f(section));
|
||||
}
|
||||
|
||||
let world = self.world_lock.read();
|
||||
let Some(chunk) = world.chunks.get(&chunk_pos) else {
|
||||
return None;
|
||||
};
|
||||
let chunk = chunk.read();
|
||||
|
||||
let sections: Vec<azalea_world::palette::PalettedContainer> = chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| section.states.clone())
|
||||
.collect();
|
||||
|
||||
if section_index >= sections.len() {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
};
|
||||
|
||||
let section = §ions[section_index];
|
||||
let r = f(section);
|
||||
|
||||
// add the sections to the chunk cache
|
||||
cached_chunks.push((chunk_pos, sections));
|
||||
|
||||
Some(r)
|
||||
}
|
||||
|
||||
fn calculate_bitsets_for_section(&self, section_pos: ChunkSectionPos) -> Option<CachedSection> {
|
||||
self.with_section(section_pos, |section| {
|
||||
let mut passable_bitset = FixedBitSet::<4096>::new();
|
||||
let mut solid_bitset = FixedBitSet::<4096>::new();
|
||||
for i in 0..4096 {
|
||||
let block_state_id = section.get_at_index(i);
|
||||
let block_state = BlockState::try_from(block_state_id).unwrap_or(BlockState::AIR);
|
||||
if is_block_state_passable(block_state) {
|
||||
passable_bitset.set(i);
|
||||
}
|
||||
if is_block_state_solid(block_state) {
|
||||
solid_bitset.set(i);
|
||||
}
|
||||
}
|
||||
CachedSection {
|
||||
pos: section_pos,
|
||||
passable_bitset,
|
||||
solid_bitset,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_block_passable(&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.passable_bitset.index(index);
|
||||
}
|
||||
|
||||
let Some(cached) = self.calculate_bitsets_for_section(section_pos) else {
|
||||
return false;
|
||||
};
|
||||
let passable = cached.passable_bitset.index(index);
|
||||
cached_blocks.insert(cached);
|
||||
passable
|
||||
}
|
||||
|
||||
pub fn is_block_solid(&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.solid_bitset.index(index);
|
||||
}
|
||||
|
||||
let Some(cached) = self.calculate_bitsets_for_section(section_pos) else {
|
||||
return false;
|
||||
};
|
||||
let solid = cached.solid_bitset.index(index);
|
||||
cached_blocks.insert(cached);
|
||||
solid
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Whether we can stand in this position. Checks if the block below is
|
||||
/// solid, and that the two blocks above that are passable.
|
||||
|
||||
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 < self.min_y {
|
||||
return u32::MAX;
|
||||
}
|
||||
}
|
||||
distance
|
||||
}
|
||||
}
|
||||
|
||||
/// whether this block is passable
|
||||
fn is_block_state_passable(block: BlockState) -> bool {
|
||||
if block.is_air() {
|
||||
// fast path
|
||||
return true;
|
||||
}
|
||||
if !block.is_shape_empty() {
|
||||
return false;
|
||||
}
|
||||
if block == azalea_registry::Block::Water.into() {
|
||||
return false;
|
||||
}
|
||||
if block.waterlogged() {
|
||||
return false;
|
||||
}
|
||||
if block == azalea_registry::Block::Lava.into() {
|
||||
return false;
|
||||
}
|
||||
// block.waterlogged currently doesn't account for seagrass and some other water
|
||||
// blocks
|
||||
if block == azalea_registry::Block::Seagrass.into() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
||||
fn is_block_state_solid(block: BlockState) -> bool {
|
||||
if block.is_air() {
|
||||
// fast path
|
||||
return false;
|
||||
}
|
||||
block.is_shape_full()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use azalea_block::BlockState;
|
||||
use azalea_core::position::ChunkPos;
|
||||
use azalea_world::{Chunk, ChunkStorage, PartialInstance};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
#[test]
|
||||
fn test_is_passable() {
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut world = ChunkStorage::default();
|
||||
|
||||
partial_world
|
||||
.chunks
|
||||
.set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
|
||||
partial_world.chunks.set_block_state(
|
||||
&BlockPos::new(0, 0, 0),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
&world,
|
||||
);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
|
||||
|
||||
let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())));
|
||||
assert!(!ctx.is_block_passable(BlockPos::new(0, 0, 0)));
|
||||
assert!(ctx.is_block_passable(BlockPos::new(0, 1, 0),));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_solid() {
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut world = ChunkStorage::default();
|
||||
partial_world
|
||||
.chunks
|
||||
.set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
|
||||
partial_world.chunks.set_block_state(
|
||||
&BlockPos::new(0, 0, 0),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
&world,
|
||||
);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
|
||||
|
||||
let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())));
|
||||
assert!(ctx.is_block_solid(BlockPos::new(0, 0, 0)));
|
||||
assert!(!ctx.is_block_solid(BlockPos::new(0, 1, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_standable() {
|
||||
let mut partial_world = PartialInstance::default();
|
||||
let mut world = ChunkStorage::default();
|
||||
partial_world
|
||||
.chunks
|
||||
.set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world);
|
||||
partial_world.chunks.set_block_state(
|
||||
&BlockPos::new(0, 0, 0),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
&world,
|
||||
);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 2, 0), BlockState::AIR, &world);
|
||||
partial_world
|
||||
.chunks
|
||||
.set_block_state(&BlockPos::new(0, 3, 0), BlockState::AIR, &world);
|
||||
|
||||
let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())));
|
||||
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)));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue