From b0bd992adcff71ee294dd05060e00e652f62a7b2 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:41:17 -0500 Subject: Fluid physics fixes (#210) * start fixing code related to fluid physics * implement force_solid for blocks * afk pool test --- azalea-physics/src/collision/entity_collisions.rs | 97 ++++++++++++ azalea-physics/src/collision/mod.rs | 10 +- azalea-physics/src/collision/shape.rs | 11 +- azalea-physics/src/collision/world_collisions.rs | 182 ++++++++++++++++++---- 4 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 azalea-physics/src/collision/entity_collisions.rs (limited to 'azalea-physics/src/collision') 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, With)>; + +pub type PhysicsQuery<'world, 'state, 'a> = + Query<'world, 'state, &'a Physics, Without>; + +pub fn get_entity_collisions( + world: &Instance, + aabb: &AABB, + source_entity: Option, + physics_query: &PhysicsQuery, + collidable_entity_query: &CollidableEntityQuery, +) -> Vec { + 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, + 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 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 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 { - let mut state = BlockCollisionsState::new(world, aabb); +pub fn get_block_collisions(world: &Instance, aabb: &AABB) -> Vec { + 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 { 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 { + 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, + 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 { // 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, +} + +impl EntityCollisionContext { + pub fn of(entity: Option) -> 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), +) { + 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); + } + } + } +} -- cgit v1.2.3