aboutsummaryrefslogtreecommitdiff
path: root/azalea-physics/src/collision
diff options
context:
space:
mode:
Diffstat (limited to 'azalea-physics/src/collision')
-rw-r--r--azalea-physics/src/collision/entity_collisions.rs97
-rw-r--r--azalea-physics/src/collision/mod.rs10
-rwxr-xr-xazalea-physics/src/collision/shape.rs11
-rw-r--r--azalea-physics/src/collision/world_collisions.rs182
4 files changed, 264 insertions, 36 deletions
diff --git a/azalea-physics/src/collision/entity_collisions.rs b/azalea-physics/src/collision/entity_collisions.rs
new file mode 100644
index 00000000..1300cf34
--- /dev/null
+++ b/azalea-physics/src/collision/entity_collisions.rs
@@ -0,0 +1,97 @@
+use azalea_core::aabb::AABB;
+use azalea_entity::{
+ LocalEntity, Physics,
+ metadata::{AbstractBoat, Shulker},
+};
+use azalea_world::Instance;
+use bevy_ecs::{
+ entity::Entity,
+ query::{Or, With, Without},
+ system::Query,
+};
+use tracing::error;
+
+use super::VoxelShape;
+
+/// This query matches on entities that we can collide with. That is, boats and
+/// shulkers.
+///
+/// If you want to use this in a more complex query, use
+/// [`CollidableEntityFilter`] as a filter instead.
+pub type CollidableEntityQuery<'world, 'state> = Query<'world, 'state, (), CollidableEntityFilter>;
+/// This filter matches on entities that we can collide with (boats and
+/// shulkers).
+///
+/// Use [`CollidableEntityQuery`] if you want an empty query that matches with
+/// this.
+pub type CollidableEntityFilter = Or<(With<AbstractBoat>, With<Shulker>)>;
+
+pub type PhysicsQuery<'world, 'state, 'a> =
+ Query<'world, 'state, &'a Physics, Without<LocalEntity>>;
+
+pub fn get_entity_collisions(
+ world: &Instance,
+ aabb: &AABB,
+ source_entity: Option<Entity>,
+ physics_query: &PhysicsQuery,
+ collidable_entity_query: &CollidableEntityQuery,
+) -> Vec<VoxelShape> {
+ if aabb.size() < 1.0E-7 {
+ return vec![];
+ }
+
+ let collision_predicate = |entity| collidable_entity_query.get(entity).is_ok();
+
+ let collidable_entities = get_entities(
+ world,
+ source_entity,
+ &aabb.inflate_all(1.0E-7),
+ &collision_predicate,
+ physics_query,
+ );
+
+ collidable_entities
+ .into_iter()
+ .map(|(_entity, aabb)| VoxelShape::from(aabb))
+ .collect()
+}
+
+/// Return all entities that are colliding with the given bounding box and match
+/// the given predicate.
+///
+/// `source_entity` is the entity that the bounding box belongs to, and won't be
+/// one of the returned entities.
+pub fn get_entities(
+ world: &Instance,
+ source_entity: Option<Entity>,
+ aabb: &AABB,
+ predicate: &dyn Fn(Entity) -> bool,
+ physics_query: &PhysicsQuery,
+) -> Vec<(Entity, AABB)> {
+ let mut matches = Vec::new();
+
+ super::world_collisions::for_entities_in_chunks_colliding_with(
+ world,
+ aabb,
+ |_chunk_pos, entities_in_chunk| {
+ // now check if the entity itself collides
+ for &candidate in entities_in_chunk {
+ if Some(candidate) != source_entity && predicate(candidate) {
+ let Ok(physics) = physics_query.get(candidate) else {
+ error!(
+ "Entity {candidate} (found from for_entities_in_chunks_colliding_with) is missing required components."
+ );
+ continue;
+ };
+
+ let candidate_aabb = physics.bounding_box;
+ if aabb.intersects_aabb(&candidate_aabb) {
+ matches.push((candidate, physics.bounding_box));
+ }
+ }
+ }
+ },
+ );
+
+ matches
+}
diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs
index 540cf7d4..77af1232 100644
--- a/azalea-physics/src/collision/mod.rs
+++ b/azalea-physics/src/collision/mod.rs
@@ -1,8 +1,9 @@
mod blocks;
mod discrete_voxel_shape;
+pub mod entity_collisions;
mod mergers;
mod shape;
-mod world_collisions;
+pub mod world_collisions;
use std::{ops::Add, sync::LazyLock};
@@ -279,7 +280,7 @@ fn collide_bounding_box(
// TODO: world border
let block_collisions =
- get_block_collisions(world, entity_bounding_box.expand_towards(movement));
+ get_block_collisions(world, &entity_bounding_box.expand_towards(movement));
collision_boxes.extend(block_collisions);
collide_with_shapes(movement, *entity_bounding_box, &collision_boxes)
}
@@ -392,6 +393,11 @@ fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
///
/// This is marked as deprecated in Minecraft.
pub fn legacy_blocks_motion(block: BlockState) -> bool {
+ if block == BlockState::AIR {
+ // fast path
+ return false;
+ }
+
let registry_block = azalea_registry::Block::from(block);
legacy_calculate_solid(block)
&& registry_block != azalea_registry::Block::Cobweb
diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs
index 726e62ad..fc5615c3 100755
--- a/azalea-physics/src/collision/shape.rs
+++ b/azalea-physics/src/collision/shape.rs
@@ -194,7 +194,7 @@ impl Shapes {
}
/// Check if the op is true anywhere when joining the two shapes
- /// vanilla calls this joinIsNotEmpty
+ /// vanilla calls this joinIsNotEmpty (join_is_not_empty).
pub fn matches_anywhere(
a: &VoxelShape,
b: &VoxelShape,
@@ -574,13 +574,18 @@ impl VoxelShape {
}
}
-impl From<AABB> for VoxelShape {
- fn from(aabb: AABB) -> Self {
+impl From<&AABB> for VoxelShape {
+ fn from(aabb: &AABB) -> Self {
box_shape(
aabb.min.x, aabb.min.y, aabb.min.z, aabb.max.x, aabb.max.y, aabb.max.z,
)
}
}
+impl From<AABB> for VoxelShape {
+ fn from(aabb: AABB) -> Self {
+ VoxelShape::from(&aabb)
+ }
+}
#[derive(Clone, PartialEq, Debug)]
pub struct ArrayVoxelShape {
diff --git a/azalea-physics/src/collision/world_collisions.rs b/azalea-physics/src/collision/world_collisions.rs
index 3aede743..ded31275 100644
--- a/azalea-physics/src/collision/world_collisions.rs
+++ b/azalea-physics/src/collision/world_collisions.rs
@@ -1,19 +1,21 @@
-use std::sync::Arc;
+use std::{collections::HashSet, sync::Arc};
-use azalea_block::BlockState;
+use azalea_block::{BlockState, fluid_state::FluidState};
use azalea_core::{
- cursor3d::{Cursor3d, CursorIterationType},
+ cursor3d::{Cursor3d, CursorIteration, CursorIterationType},
math::EPSILON,
- position::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos, ChunkSectionPos},
+ position::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos, ChunkSectionPos, Vec3},
};
+use azalea_inventory::ItemStack;
use azalea_world::{Chunk, Instance};
+use bevy_ecs::entity::Entity;
use parking_lot::RwLock;
use super::{BLOCK_SHAPE, Shapes};
use crate::collision::{AABB, BlockWithShape, VoxelShape};
-pub fn get_block_collisions(world: &Instance, aabb: AABB) -> Vec<VoxelShape> {
- let mut state = BlockCollisionsState::new(world, aabb);
+pub fn get_block_collisions(world: &Instance, aabb: &AABB) -> Vec<VoxelShape> {
+ let mut state = BlockCollisionsState::new(world, aabb, EntityCollisionContext::of(None));
let mut block_collisions = Vec::new();
let initial_chunk_pos = ChunkPos::from(state.cursor.origin());
@@ -21,25 +23,80 @@ pub fn get_block_collisions(world: &Instance, aabb: AABB) -> Vec<VoxelShape> {
let initial_chunk = initial_chunk.as_deref().map(RwLock::read);
while let Some(item) = state.cursor.next() {
+ state.compute_next(
+ item,
+ &mut block_collisions,
+ initial_chunk_pos,
+ initial_chunk.as_deref(),
+ );
+ }
+
+ block_collisions
+}
+
+pub fn get_block_and_liquid_collisions(world: &Instance, aabb: &AABB) -> Vec<VoxelShape> {
+ let mut state = BlockCollisionsState::new(
+ world,
+ aabb,
+ EntityCollisionContext::of(None).with_include_liquids(true),
+ );
+ let mut block_collisions = Vec::new();
+
+ let initial_chunk_pos = ChunkPos::from(state.cursor.origin());
+ let initial_chunk = world.chunks.get(&initial_chunk_pos);
+ let initial_chunk = initial_chunk.as_deref().map(RwLock::read);
+
+ while let Some(item) = state.cursor.next() {
+ state.compute_next(
+ item,
+ &mut block_collisions,
+ initial_chunk_pos,
+ initial_chunk.as_deref(),
+ );
+ }
+
+ block_collisions
+}
+
+pub struct BlockCollisionsState<'a> {
+ pub world: &'a Instance,
+ pub aabb: &'a AABB,
+ pub entity_shape: VoxelShape,
+ pub cursor: Cursor3d,
+
+ _context: EntityCollisionContext,
+
+ cached_sections: Vec<(ChunkSectionPos, azalea_world::Section)>,
+ cached_block_shapes: Vec<(BlockState, &'static VoxelShape)>,
+}
+
+impl<'a> BlockCollisionsState<'a> {
+ fn compute_next(
+ &mut self,
+ item: CursorIteration,
+ block_collisions: &mut Vec<VoxelShape>,
+ initial_chunk_pos: ChunkPos,
+ initial_chunk: Option<&Chunk>,
+ ) {
if item.iteration_type == CursorIterationType::Corner {
- continue;
+ return;
}
let item_chunk_pos = ChunkPos::from(item.pos);
let block_state: BlockState = if item_chunk_pos == initial_chunk_pos {
match &initial_chunk {
Some(initial_chunk) => initial_chunk
- .get(&ChunkBlockPos::from(item.pos), state.world.chunks.min_y)
+ .get(&ChunkBlockPos::from(item.pos), self.world.chunks.min_y)
.unwrap_or(BlockState::AIR),
_ => BlockState::AIR,
}
} else {
- state.get_block_state(item.pos)
+ self.get_block_state(item.pos)
};
if block_state.is_air() {
// fast path since we can't collide with air
- continue;
+ return;
}
// TODO: continue if self.only_suffocating_blocks and the block is not
@@ -47,43 +104,29 @@ pub fn get_block_collisions(world: &Instance, aabb: AABB) -> Vec<VoxelShape> {
// if it's a full block do a faster collision check
if block_state.is_collision_shape_full() {
- if !state.aabb.intersects_aabb(&AABB {
+ if !self.aabb.intersects_aabb(&AABB {
min: item.pos.to_vec3_floored(),
max: (item.pos + 1).to_vec3_floored(),
}) {
- continue;
+ return;
}
block_collisions.push(BLOCK_SHAPE.move_relative(item.pos.to_vec3_floored()));
- continue;
+ return;
}
- let block_shape = state.get_block_shape(block_state);
+ let block_shape = self.get_block_shape(block_state);
let block_shape = block_shape.move_relative(item.pos.to_vec3_floored());
// if the entity shape and block shape don't collide, continue
- if !Shapes::matches_anywhere(&block_shape, &state.entity_shape, |a, b| a && b) {
- continue;
+ if !Shapes::matches_anywhere(&block_shape, &self.entity_shape, |a, b| a && b) {
+ return;
}
block_collisions.push(block_shape);
}
- block_collisions
-}
-
-pub struct BlockCollisionsState<'a> {
- pub world: &'a Instance,
- pub aabb: AABB,
- pub entity_shape: VoxelShape,
- pub cursor: Cursor3d,
-
- cached_sections: Vec<(ChunkSectionPos, azalea_world::Section)>,
- cached_block_shapes: Vec<(BlockState, &'static VoxelShape)>,
-}
-
-impl<'a> BlockCollisionsState<'a> {
- pub fn new(world: &'a Instance, aabb: AABB) -> Self {
+ pub fn new(world: &'a Instance, aabb: &'a AABB, context: EntityCollisionContext) -> Self {
let origin = BlockPos {
x: (aabb.min.x - EPSILON).floor() as i32 - 1,
y: (aabb.min.y - EPSILON).floor() as i32 - 1,
@@ -104,6 +147,8 @@ impl<'a> BlockCollisionsState<'a> {
entity_shape: VoxelShape::from(aabb),
cursor,
+ _context: context,
+
cached_sections: Vec::new(),
cached_block_shapes: Vec::new(),
}
@@ -182,3 +227,78 @@ impl<'a> BlockCollisionsState<'a> {
shape
}
}
+
+pub struct EntityCollisionContext {
+ pub descending: bool,
+ pub entity_bottom: f64,
+ pub held_item: ItemStack,
+ can_stand_on_fluid_predicate: CanStandOnFluidPredicate,
+ pub entity: Option<Entity>,
+}
+
+impl EntityCollisionContext {
+ pub fn of(entity: Option<Entity>) -> Self {
+ Self {
+ descending: false,
+ entity_bottom: 0.0,
+ held_item: ItemStack::Empty,
+ can_stand_on_fluid_predicate: CanStandOnFluidPredicate::PassToEntity,
+ entity,
+ }
+ }
+ pub fn with_include_liquids(mut self, include_liquids: bool) -> Self {
+ self.can_stand_on_fluid_predicate = if include_liquids {
+ CanStandOnFluidPredicate::AlwaysTrue
+ } else {
+ CanStandOnFluidPredicate::PassToEntity
+ };
+ self
+ }
+
+ pub fn can_stand_on_fluid(&self, above: &FluidState, target: &FluidState) -> bool {
+ self.can_stand_on_fluid_predicate.matches(target) && !above.is_same_kind(target)
+ }
+}
+
+enum CanStandOnFluidPredicate {
+ PassToEntity,
+ AlwaysTrue,
+}
+impl CanStandOnFluidPredicate {
+ pub fn matches(&self, _state: &FluidState) -> bool {
+ match self {
+ Self::AlwaysTrue => true,
+ // minecraft sometimes returns true for striders here, false for every other entity
+ // though
+ Self::PassToEntity => false,
+ }
+ }
+}
+
+/// This basically gets all the chunks that an entity colliding with
+/// that bounding box could be in.
+///
+/// This is forEachAccessibleNonEmptySection in vanilla Minecraft because they
+/// sort entities into sections instead of just chunks. In theory this might be
+/// a performance loss for Azalea. If this ever turns out to be a bottleneck,
+/// then maybe you should try having it do that instead.
+pub fn for_entities_in_chunks_colliding_with(
+ world: &Instance,
+ aabb: &AABB,
+ mut consumer: impl FnMut(ChunkPos, &HashSet<Entity>),
+) {
+ let min_section = ChunkSectionPos::from(aabb.min - Vec3::new(2., 4., 2.));
+ let max_section = ChunkSectionPos::from(aabb.max + Vec3::new(2., 0., 2.));
+
+ let min_chunk = ChunkPos::from(min_section);
+ let max_chunk = ChunkPos::from(max_section);
+
+ for chunk_x in min_chunk.x..=max_chunk.x {
+ for chunk_z in min_chunk.z..=max_chunk.z {
+ let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
+ if let Some(entities) = world.entities_by_chunk.get(&chunk_pos) {
+ consumer(chunk_pos, entities);
+ }
+ }
+ }
+}