1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 23:44:38 +00:00

bubble columns

This commit is contained in:
mat 2025-01-10 07:17:06 +00:00
commit 579aefdf68
18 changed files with 1273 additions and 847 deletions

View file

@ -5,7 +5,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct FluidState {
pub fluid: azalea_registry::Fluid,
pub kind: FluidKind,
/// 0 = empty, 8 = full, 9 = max.
///
/// 9 is meant to be used when there's another fluid block of the same type
@ -23,42 +23,44 @@ pub struct FluidState {
/// set (see FlowingFluid.getFlowing)
pub falling: bool,
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub enum FluidKind {
#[default]
Empty,
Water,
Lava,
}
impl FluidState {
pub fn new_source_block(kind: FluidKind, falling: bool) -> Self {
Self {
kind,
amount: 8,
falling,
}
}
/// A floating point number in between 0 and 1 representing the height (as a
/// percentage of a full block) of the fluid.
pub fn height(&self) -> f32 {
self.amount as f32 / 9.
}
pub fn is_empty(&self) -> bool {
self.amount == 0
}
pub fn affects_flow(&self, other: &FluidState) -> bool {
other.amount == 0 || self.is_same_kind(other)
}
pub fn is_same_kind(&self, other: &FluidState) -> bool {
(other.is_water() && self.is_water())
|| (other.is_lava() && self.is_lava())
|| (self.amount == 0 && other.amount == 0)
}
pub fn is_water(&self) -> bool {
matches!(
self.fluid,
azalea_registry::Fluid::Water | azalea_registry::Fluid::FlowingWater
)
}
pub fn is_lava(&self) -> bool {
matches!(
self.fluid,
azalea_registry::Fluid::Lava | azalea_registry::Fluid::FlowingLava
)
(other.kind == self.kind) || (self.amount == 0 && other.amount == 0)
}
}
impl Default for FluidState {
fn default() -> Self {
Self {
fluid: azalea_registry::Fluid::Empty,
kind: FluidKind::Empty,
amount: 0,
falling: false,
}
@ -74,38 +76,47 @@ impl From<BlockState> for FluidState {
.property::<crate::properties::Waterlogged>()
.unwrap_or_default()
{
Self {
fluid: azalea_registry::Fluid::Water,
return Self {
kind: FluidKind::Water,
amount: 8,
falling: false,
}
} else {
let block = Box::<dyn Block>::from(state);
if let Some(water) = block.downcast_ref::<crate::blocks::Water>() {
Self {
fluid: azalea_registry::Fluid::Water,
amount: to_or_from_legacy_fluid_level(water.level as u8),
falling: false,
}
} else if let Some(lava) = block.downcast_ref::<crate::blocks::Lava>() {
Self {
fluid: azalea_registry::Fluid::Lava,
amount: to_or_from_legacy_fluid_level(lava.level as u8),
falling: false,
}
} else {
Self {
fluid: azalea_registry::Fluid::Empty,
amount: 0,
falling: false,
}
}
};
}
let registry_block = azalea_registry::Block::from(state);
match registry_block {
azalea_registry::Block::Water => {
let level = state
.property::<crate::properties::WaterLevel>()
.expect("water block should always have WaterLevel");
return Self {
kind: FluidKind::Water,
amount: to_or_from_legacy_fluid_level(level as u8),
falling: false,
};
}
azalea_registry::Block::Lava => {
let level = state
.property::<crate::properties::LavaLevel>()
.expect("lava block should always have LavaLevel");
return Self {
kind: FluidKind::Lava,
amount: to_or_from_legacy_fluid_level(level as u8),
falling: false,
};
}
azalea_registry::Block::BubbleColumn => {
return Self::new_source_block(FluidKind::Water, false);
}
_ => {}
}
Self::default()
}
}
/// Sometimes Minecraft represents fluids with 0 being the empty and 8 being
/// full, and sometimes it's the opposite. You can use this function to convert
/// Sometimes Minecraft represents fluids with 0 being empty and 8 being full,
/// and sometimes it's the opposite. You can use this function to convert
/// in between those two representations.
///
/// You usually don't need to call this yourself, see [`FluidState`].
@ -116,22 +127,14 @@ pub fn to_or_from_legacy_fluid_level(level: u8) -> u8 {
impl From<FluidState> for BlockState {
fn from(state: FluidState) -> Self {
match state.fluid {
azalea_registry::Fluid::Empty => BlockState::AIR,
azalea_registry::Fluid::Water | azalea_registry::Fluid::FlowingWater => {
BlockState::from(crate::blocks::Water {
level: crate::properties::WaterLevel::from(
state.amount as BlockStateIntegerRepr,
),
})
}
azalea_registry::Fluid::Lava | azalea_registry::Fluid::FlowingLava => {
BlockState::from(crate::blocks::Lava {
level: crate::properties::LavaLevel::from(
state.amount as BlockStateIntegerRepr,
),
})
}
match state.kind {
FluidKind::Empty => BlockState::AIR,
FluidKind::Water => BlockState::from(crate::blocks::Water {
level: crate::properties::WaterLevel::from(state.amount as BlockStateIntegerRepr),
}),
FluidKind::Lava => BlockState::from(crate::blocks::Lava {
level: crate::properties::LavaLevel::from(state.amount as BlockStateIntegerRepr),
}),
}
}
}

View file

@ -500,6 +500,9 @@ pub fn process_packet_events(ecs: &mut World) {
**position = new_pos;
}
// old_pos is set to the current position when we're teleported
physics.set_old_pos(&position);
// send the relevant packets
send_packet_events.send(SendPacketEvent::new(
@ -853,10 +856,14 @@ pub fn process_packet_events(ecs: &mut World) {
if new_pos != **position {
**position = new_pos;
}
let position = position.clone();
let mut look_direction = entity.get_mut::<LookDirection>().unwrap();
if new_look_direction != *look_direction {
*look_direction = new_look_direction;
}
// old_pos is set to the current position when we're teleported
let mut physics = entity.get_mut::<Physics>().unwrap();
physics.set_old_pos(&position);
}),
});

View file

@ -18,8 +18,8 @@ pub struct ClipPointOpts<'a> {
pub delta: &'a Vec3,
pub begin: f64,
pub min_x: f64,
pub max_x: f64,
pub min_z: f64,
pub max_x: f64,
pub max_z: f64,
pub result_dir: Direction,
pub start: &'a Vec3,
@ -27,36 +27,28 @@ pub struct ClipPointOpts<'a> {
impl AABB {
pub fn contract(&self, x: f64, y: f64, z: f64) -> AABB {
let mut min_x = self.min.x;
let mut min_y = self.min.y;
let mut min_z = self.min.z;
let mut max_x = self.max.x;
let mut max_y = self.max.y;
let mut max_z = self.max.z;
let mut min = self.min;
let mut max = self.max;
if x < 0.0 {
min_x -= x;
min.x -= x;
} else if x > 0.0 {
max_x -= x;
max.x -= x;
}
if y < 0.0 {
min_y -= y;
min.y -= y;
} else if y > 0.0 {
max_y -= y;
max.y -= y;
}
if z < 0.0 {
min_z -= z;
min.z -= z;
} else if z > 0.0 {
max_z -= z;
max.z -= z;
}
AABB {
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
}
AABB { min, max }
}
pub fn expand_towards(&self, other: &Vec3) -> AABB {
@ -167,13 +159,13 @@ impl AABB {
})
}
pub fn contains(&self, x: f64, y: f64, z: f64) -> bool {
x >= self.min.x
&& x < self.max.x
&& y >= self.min.y
&& y < self.max.y
&& z >= self.min.z
&& z < self.max.z
pub fn contains(&self, point: &Vec3) -> bool {
point.x >= self.min.x
&& point.x < self.max.x
&& point.y >= self.min.y
&& point.y < self.max.y
&& point.z >= self.min.z
&& point.z < self.max.z
}
pub fn size(&self) -> f64 {
@ -202,10 +194,17 @@ impl AABB {
pub fn clip(&self, min: &Vec3, max: &Vec3) -> Option<Vec3> {
let mut t = 1.0;
let delta = max - min;
let _dir = Self::get_direction(self, min, &mut t, None, &delta)?;
let _dir = Self::get_direction_aabb(self, min, &mut t, None, &delta)?;
Some(min + &(delta * t))
}
pub fn clip_with_from_and_to(min: &Vec3, max: &Vec3, from: &Vec3, to: &Vec3) -> Option<Vec3> {
let mut t = 1.0;
let delta = to - from;
let _dir = Self::get_direction(min, max, from, &mut t, None, &delta)?;
Some(from + &(delta * t))
}
pub fn clip_iterable(
boxes: &Vec<AABB>,
from: &Vec3,
@ -217,7 +216,7 @@ impl AABB {
let delta = to - from;
for aabb in boxes {
dir = Self::get_direction(
dir = Self::get_direction_aabb(
&aabb.move_relative(pos.to_vec3_floored()),
from,
&mut t,
@ -236,8 +235,19 @@ impl AABB {
})
}
fn get_direction_aabb(
&self,
from: &Vec3,
t: &mut f64,
dir: Option<Direction>,
delta: &Vec3,
) -> Option<Direction> {
AABB::get_direction(&self.min, &self.max, from, t, dir, delta)
}
fn get_direction(
aabb: &AABB,
min: &Vec3,
max: &Vec3,
from: &Vec3,
t: &mut f64,
mut dir: Option<Direction>,
@ -248,11 +258,11 @@ impl AABB {
t,
approach_dir: dir,
delta,
begin: aabb.min.x,
min_x: aabb.min.y,
max_x: aabb.max.y,
min_z: aabb.min.z,
max_z: aabb.max.z,
begin: min.x,
min_x: min.y,
max_x: max.y,
min_z: min.z,
max_z: max.z,
result_dir: Direction::West,
start: from,
});
@ -261,11 +271,11 @@ impl AABB {
t,
approach_dir: dir,
delta,
begin: aabb.max.x,
min_x: aabb.min.y,
max_x: aabb.max.y,
min_z: aabb.min.z,
max_z: aabb.max.z,
begin: max.x,
min_x: min.y,
max_x: max.y,
min_z: min.z,
max_z: max.z,
result_dir: Direction::East,
start: from,
});
@ -280,11 +290,11 @@ impl AABB {
y: delta.z,
z: delta.x,
},
begin: aabb.min.y,
min_x: aabb.min.z,
max_x: aabb.max.z,
min_z: aabb.min.x,
max_z: aabb.max.x,
begin: min.y,
min_x: min.z,
max_x: max.z,
min_z: min.x,
max_z: max.x,
result_dir: Direction::Down,
start: &Vec3 {
x: from.y,
@ -301,11 +311,11 @@ impl AABB {
y: delta.z,
z: delta.x,
},
begin: aabb.max.y,
min_x: aabb.min.z,
max_x: aabb.max.z,
min_z: aabb.min.x,
max_z: aabb.max.x,
begin: max.y,
min_x: min.z,
max_x: max.z,
min_z: min.x,
max_z: max.x,
result_dir: Direction::Up,
start: &Vec3 {
x: from.y,
@ -324,11 +334,11 @@ impl AABB {
y: delta.x,
z: delta.y,
},
begin: aabb.min.z,
min_x: aabb.min.x,
max_x: aabb.max.x,
min_z: aabb.min.y,
max_z: aabb.max.y,
begin: min.z,
min_x: min.x,
max_x: max.x,
min_z: min.y,
max_z: max.y,
result_dir: Direction::North,
start: &Vec3 {
x: from.z,
@ -345,11 +355,11 @@ impl AABB {
y: delta.x,
z: delta.y,
},
begin: aabb.max.z,
min_x: aabb.min.x,
max_x: aabb.max.x,
min_z: aabb.min.y,
max_z: aabb.max.y,
begin: max.z,
min_x: min.x,
max_x: max.x,
min_z: min.y,
max_z: max.y,
result_dir: Direction::South,
start: &Vec3 {
x: from.z,
@ -418,6 +428,60 @@ impl AABB {
pub fn min(&self, axis: &Axis) -> f64 {
axis.choose(self.min.x, self.min.y, self.min.z)
}
pub fn collided_along_vector(&self, vector: Vec3, boxes: &Vec<AABB>) -> bool {
let center = self.get_center();
let new_center = center + vector;
for aabb in boxes {
let inflated = aabb.inflate(
self.get_size(Axis::X) * 0.5,
self.get_size(Axis::Y) * 0.5,
self.get_size(Axis::Z) * 0.5,
);
if inflated.contains(&new_center) || inflated.contains(&center) {
return true;
}
if inflated.clip(&center, &new_center).is_some() {
return true;
}
}
false
}
}
impl BlockPos {
pub fn between_closed_aabb(aabb: &AABB) -> Vec<BlockPos> {
BlockPos::between_closed(BlockPos::from(aabb.min), BlockPos::from(aabb.max))
}
pub fn between_closed(min: BlockPos, max: BlockPos) -> Vec<BlockPos> {
assert!(min.x <= max.x);
assert!(min.y <= max.y);
assert!(min.z <= max.z);
let length_x = max.x - min.x + 1;
let length_y = max.y - min.y + 1;
let length_z = max.z - min.z + 1;
let volume = length_x * length_y * length_z;
let mut result = Vec::with_capacity(volume as usize);
for index in 0..volume {
let index_x = index % length_x;
let remaining_after_x = index / length_x;
let index_y = remaining_after_x % length_y;
let index_z = remaining_after_x / length_y;
result.push(BlockPos::new(
min.x + index_x,
min.y + index_y,
min.z + index_z,
));
}
result
}
}
#[cfg(test)]

View file

@ -99,6 +99,7 @@ pub enum CardinalDirection {
East,
}
/// A 3D axis like x, y, z.
#[derive(Clone, Copy, Debug)]
pub enum Axis {
X = 0,

View file

@ -86,6 +86,25 @@ pub fn to_degrees(radians: f64) -> f64 {
radians * 57.29577951308232
}
/// Returns either -1, 0, or 1, depending on whether the number is negative,
/// zero, or positive.
///
/// This function exists because f64::signum doesn't check for 0.
pub fn sign(num: f64) -> f64 {
if num == 0. {
0.
} else {
num.signum()
}
}
pub fn sign_as_int(num: f64) -> i32 {
if num == 0. {
0
} else {
num.signum() as i32
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -7,7 +7,7 @@ pub struct EntityDimensions {
}
impl EntityDimensions {
pub fn make_bounding_box(&self, pos: Vec3) -> AABB {
pub fn make_bounding_box(&self, pos: &Vec3) -> AABB {
let radius = (self.width / 2.0) as f64;
let height = self.height as f64;
AABB {

View file

@ -12,13 +12,12 @@ mod plugin;
pub mod vec_delta_codec;
use std::{
collections::HashMap,
fmt::Debug,
hash::{Hash, Hasher},
};
pub use attributes::Attributes;
use azalea_block::BlockState;
use azalea_block::{fluid_state::FluidKind, BlockState};
use azalea_buf::AzBuf;
use azalea_core::{
aabb::AABB,
@ -257,6 +256,11 @@ pub struct Physics {
pub velocity: Vec3,
pub vec_delta_codec: VecDeltaCodec,
/// The position of the entity before it moved this tick.
///
/// This is set immediately before physics is done.
pub old_position: Vec3,
/// The acceleration here is the force that will be attempted to be added to
/// the entity's velocity next tick.
///
@ -297,6 +301,8 @@ impl Physics {
velocity: Vec3::default(),
vec_delta_codec: VecDeltaCodec::new(pos),
old_position: pos,
x_acceleration: 0.,
y_acceleration: 0.,
z_acceleration: 0.,
@ -304,7 +310,7 @@ impl Physics {
on_ground: false,
last_on_ground: false,
bounding_box: dimensions.make_bounding_box(pos),
bounding_box: dimensions.make_bounding_box(&pos),
dimensions,
has_impulse: false,
@ -355,6 +361,10 @@ impl Physics {
// TODO: also check `!this.firstTick &&`
self.lava_fluid_height > 0.
}
pub fn set_old_pos(&mut self, pos: &Position) {
self.old_position = **pos;
}
}
/// Marker component for entities that are dead.
@ -458,7 +468,7 @@ impl EntityBundle {
},
jumping: Jumping(false),
fluid_on_eyes: FluidOnEyes(azalea_registry::Fluid::Empty),
fluid_on_eyes: FluidOnEyes(FluidKind::Empty),
on_climbable: OnClimbable(false),
}
}
@ -479,10 +489,10 @@ pub struct PlayerBundle {
pub struct LocalEntity;
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct FluidOnEyes(azalea_registry::Fluid);
pub struct FluidOnEyes(FluidKind);
impl FluidOnEyes {
pub fn new(fluid: azalea_registry::Fluid) -> Self {
pub fn new(fluid: FluidKind) -> Self {
Self(fluid)
}
}

View file

@ -1,4 +1,4 @@
use azalea_block::{Block, BlockBehavior};
use azalea_block::{fluid_state::FluidKind, Block, BlockBehavior};
use azalea_core::tier::get_item_tier;
use azalea_registry as registry;
@ -105,7 +105,7 @@ fn destroy_speed(
base_destroy_speed *= multiplier;
}
if registry::tags::fluids::WATER.contains(fluid_on_eyes)
if **fluid_on_eyes == FluidKind::Water
&& enchantments::get_enchant_level(registry::Enchantment::AquaAffinity, player_inventory)
== 0
{

View file

@ -3,7 +3,7 @@ mod relative_updates;
use std::collections::HashSet;
use azalea_block::BlockState;
use azalea_block::{fluid_state::FluidKind, BlockState};
use azalea_core::position::{BlockPos, ChunkPos, Vec3};
use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId};
use bevy_app::{App, Plugin, PreUpdate, Update};
@ -106,9 +106,9 @@ pub fn update_fluid_on_eyes(
.unwrap_or_default();
let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
if fluid_cutoff_y > adjusted_eye_y {
**fluid_on_eyes = fluid_at_eye.fluid;
**fluid_on_eyes = fluid_at_eye.kind;
} else {
**fluid_on_eyes = azalea_registry::Fluid::Empty;
**fluid_on_eyes = FluidKind::Empty;
}
}
}
@ -198,7 +198,7 @@ pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) {
/// Cached position in the world must be updated.
pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed<Position>>) {
for (position, mut physics) in query.iter_mut() {
let bounding_box = physics.dimensions.make_bounding_box(**position);
let bounding_box = physics.dimensions.make_bounding_box(&position);
physics.bounding_box = bounding_box;
}
}

View file

@ -1,7 +1,13 @@
use azalea_block::{fluid_state::FluidState, BlockState};
use std::collections::HashSet;
use azalea_block::{
fluid_state::{FluidKind, FluidState},
BlockState,
};
use azalea_core::{
aabb::AABB,
block_hit_result::BlockHitResult,
direction::Direction,
direction::{Axis, Direction},
math::{self, lerp, EPSILON},
position::{BlockPos, Vec3},
};
@ -80,8 +86,8 @@ impl FluidPickType {
match self {
Self::None => false,
Self::SourceOnly => fluid_state.amount == 8,
Self::Any => fluid_state.fluid != azalea_registry::Fluid::Empty,
Self::Water => fluid_state.fluid == azalea_registry::Fluid::Water,
Self::Any => fluid_state.kind != FluidKind::Empty,
Self::Water => fluid_state.kind == FluidKind::Water,
}
}
}
@ -198,22 +204,10 @@ pub fn traverse_blocks<C, T>(
let vec = right_after_end - right_before_start;
/// Returns either -1, 0, or 1, depending on whether the number is negative,
/// zero, or positive.
///
/// This function exists because f64::signum doesn't check for 0.
fn get_number_sign(num: f64) -> f64 {
if num == 0. {
0.
} else {
num.signum()
}
}
let vec_sign = Vec3 {
x: get_number_sign(vec.x),
y: get_number_sign(vec.y),
z: get_number_sign(vec.z),
x: math::sign(vec.x),
y: math::sign(vec.y),
z: math::sign(vec.z),
};
#[rustfmt::skip]
@ -270,3 +264,125 @@ pub fn traverse_blocks<C, T>(
}
}
}
pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet<BlockPos> {
let delta = to - from;
let traversed_blocks = BlockPos::between_closed_aabb(aabb);
if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 {
return traversed_blocks.into_iter().collect();
}
let mut traversed_and_collided_blocks = HashSet::new();
let target_min_pos = aabb.min;
let from_min_pos = target_min_pos - delta;
add_collisions_along_travel(
&mut traversed_and_collided_blocks,
from_min_pos,
target_min_pos,
*aabb,
);
traversed_and_collided_blocks.extend(traversed_blocks);
traversed_and_collided_blocks
}
pub fn add_collisions_along_travel(
collisions: &mut HashSet<BlockPos>,
from: Vec3,
to: Vec3,
aabb: AABB,
) {
let delta = to - from;
let mut min_x = from.x.floor() as i32;
let mut min_y = from.y.floor() as i32;
let mut min_z = from.z.floor() as i32;
let direction_x = math::sign_as_int(delta.x);
let direction_y = math::sign_as_int(delta.y);
let direction_z = math::sign_as_int(delta.z);
let step_x = if direction_x == 0 {
f64::MAX
} else {
direction_x as f64 / delta.x
};
let step_y = if direction_y == 0 {
f64::MAX
} else {
direction_y as f64 / delta.y
};
let step_z = if direction_z == 0 {
f64::MAX
} else {
direction_z as f64 / delta.z
};
let mut cur_x = step_x
* if direction_x > 0 {
1. - math::fract(from.x)
} else {
math::fract(from.x)
};
let mut cur_y = step_y
* if direction_y > 0 {
1. - math::fract(from.y)
} else {
math::fract(from.y)
};
let mut cur_z = step_z
* if direction_z > 0 {
1. - math::fract(from.z)
} else {
math::fract(from.z)
};
let mut step_count = 0;
while cur_x <= 1. || cur_y <= 1. || cur_z <= 1. {
if cur_x < cur_y {
if cur_x < cur_z {
min_x += direction_x;
cur_x += step_x;
} else {
min_z += direction_z;
cur_z += step_z;
}
} else if cur_y < cur_z {
min_y += direction_y;
cur_y += step_y;
} else {
min_z += direction_z;
cur_z += step_z;
}
if step_count > 16 {
break;
}
step_count += 1;
let Some(clip_location) = AABB::clip_with_from_and_to(
&Vec3::new(min_x as f64, min_y as f64, min_z as f64),
&Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64),
&from,
&to,
) else {
continue;
};
let initial_max_x = clip_location
.x
.clamp(min_x as f64 + 1.0E-5, min_x as f64 + 1.0 - 1.0E-5);
let initial_max_y = clip_location
.y
.clamp(min_y as f64 + 1.0E-5, min_y as f64 + 1.0 - 1.0E-5);
let initial_max_z = clip_location
.z
.clamp(min_z as f64 + 1.0E-5, min_z as f64 + 1.0 - 1.0E-5);
let max_x = (initial_max_x + aabb.get_size(Axis::X)).floor() as i32;
let max_y = (initial_max_y + aabb.get_size(Axis::Y)).floor() as i32;
let max_z = (initial_max_z + aabb.get_size(Axis::Z)).floor() as i32;
for x in min_x..=max_x {
for y in min_y..=max_y {
for z in min_z..=max_z {
collisions.insert(BlockPos::new(x, y, z));
}
}
}
}
}

View file

@ -355,7 +355,7 @@ pub fn fluid_shape(
) -> &'static VoxelShape {
if fluid.amount == 9 {
let fluid_state_above = world.get_fluid_state(&pos.up(1)).unwrap_or_default();
if fluid_state_above.fluid == fluid.fluid {
if fluid_state_above.kind == fluid.kind {
return &BLOCK_SHAPE;
}
}

View file

@ -381,16 +381,25 @@ impl VoxelShape {
}
#[must_use]
pub fn move_relative(&self, x: f64, y: f64, z: f64) -> VoxelShape {
pub fn move_relative(&self, delta: Vec3) -> VoxelShape {
if self.shape().is_empty() {
return EMPTY_SHAPE.clone();
}
VoxelShape::Array(ArrayVoxelShape::new(
self.shape().to_owned(),
self.get_coords(Axis::X).iter().map(|c| c + x).collect(),
self.get_coords(Axis::Y).iter().map(|c| c + y).collect(),
self.get_coords(Axis::Z).iter().map(|c| c + z).collect(),
self.get_coords(Axis::X)
.iter()
.map(|c| c + delta.x)
.collect(),
self.get_coords(Axis::Y)
.iter()
.map(|c| c + delta.y)
.collect(),
self.get_coords(Axis::Z)
.iter()
.map(|c| c + delta.z)
.collect(),
))
}

View file

@ -55,18 +55,13 @@ pub fn get_block_collisions(world: &Instance, aabb: AABB) -> Vec<VoxelShape> {
continue;
}
block_collisions.push(BLOCK_SHAPE.move_relative(
item.pos.x as f64,
item.pos.y as f64,
item.pos.z as f64,
));
block_collisions.push(BLOCK_SHAPE.move_relative(item.pos.to_vec3_floored()));
continue;
}
let block_shape = state.get_block_shape(block_state);
let block_shape =
block_shape.move_relative(item.pos.x as f64, item.pos.y as f64, item.pos.z as f64);
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;

View file

@ -1,12 +1,13 @@
use std::cmp;
use azalea_block::{fluid_state::FluidState, BlockState};
use azalea_block::{
fluid_state::{FluidKind, FluidState},
BlockState,
};
use azalea_core::{
direction::Direction,
position::{BlockPos, Vec3},
};
use azalea_entity::{metadata::AbstractBoat, InLoadedChunk, LocalEntity, Physics, Position};
use azalea_registry::{EntityKind, Fluid};
use azalea_entity::{InLoadedChunk, LocalEntity, Physics, Position};
use azalea_registry::Fluid;
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_ecs::prelude::*;
@ -50,7 +51,7 @@ fn update_in_water_state_and_do_water_current_pushing(
// }
// updateFluidHeightAndDoFluidPushing
if update_fluid_height_and_do_fluid_pushing(physics, world, Fluid::Water, 0.014) {
if update_fluid_height_and_do_fluid_pushing(physics, world, FluidKind::Water, 0.014) {
// if !was_touching_water && !first_tick {
// do_water_splash_effect();
// }
@ -66,7 +67,7 @@ fn update_in_water_state_and_do_water_current_pushing(
fn update_fluid_height_and_do_fluid_pushing(
physics: &mut Physics,
world: &Instance,
checking_fluid: Fluid,
checking_fluid: FluidKind,
fluid_push_factor: f64,
) -> bool {
// if touching_unloaded_chunk() {
@ -96,7 +97,7 @@ fn update_fluid_height_and_do_fluid_pushing(
let Some(fluid_at_cur_pos) = world.get_fluid_state(&cur_pos) else {
continue;
};
if fluid_at_cur_pos.fluid != checking_fluid {
if fluid_at_cur_pos.kind != checking_fluid {
continue;
}
let fluid_max_y = (cur_y as f32 + fluid_at_cur_pos.height()) as f64;
@ -146,9 +147,9 @@ fn update_fluid_height_and_do_fluid_pushing(
}
match checking_fluid {
Fluid::Water => physics.water_fluid_height = min_height_touching,
Fluid::Lava => physics.lava_fluid_height = min_height_touching,
checking_fluid => panic!("unknown fluid {checking_fluid}"),
FluidKind::Water => physics.water_fluid_height = min_height_touching,
FluidKind::Lava => physics.lava_fluid_height = min_height_touching,
FluidKind::Empty => panic!("FluidKind::Empty should not be passed to update_fluid_height"),
};
touching_fluid

View file

@ -4,8 +4,11 @@
pub mod clip;
pub mod collision;
pub mod fluids;
pub mod travel;
use azalea_block::{Block, BlockState};
use std::collections::HashSet;
use azalea_block::{fluid_state::FluidState, properties, Block, BlockState};
use azalea_core::{
aabb::AABB,
math,
@ -13,8 +16,8 @@ use azalea_core::{
tick::GameTick,
};
use azalea_entity::{
metadata::Sprinting, move_relative, Attributes, InLoadedChunk, Jumping, LocalEntity,
LookDirection, OnClimbable, Physics, Pose, Position,
metadata::Sprinting, move_relative, Attributes, InLoadedChunk, Jumping, LastSentPosition,
LocalEntity, LookDirection, OnClimbable, Physics, Pose, Position,
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, Plugin};
@ -24,7 +27,8 @@ use bevy_ecs::{
system::{Query, Res},
world::Mut,
};
use collision::{move_colliding, MoverType};
use clip::box_traverse_blocks;
use collision::{move_colliding, BlockWithShape, MoverType, VoxelShape, BLOCK_SHAPE};
/// A Bevy [`SystemSet`] for running physics that makes entities do things.
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
@ -38,9 +42,11 @@ impl Plugin for PhysicsPlugin {
(
fluids::update_in_water_state_and_do_fluid_pushing
.before(azalea_entity::update_fluid_on_eyes),
update_old_position,
fluids::update_swimming.after(azalea_entity::update_fluid_on_eyes),
ai_step,
travel,
travel::travel,
apply_effects_from_blocks,
)
.chain()
.in_set(PhysicsSet)
@ -49,303 +55,9 @@ impl Plugin for PhysicsPlugin {
}
}
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
#[allow(clippy::type_complexity)]
fn travel(
mut query: Query<
(
&mut Physics,
&mut LookDirection,
&mut Position,
Option<&Sprinting>,
Option<&Pose>,
&Attributes,
&InstanceName,
&OnClimbable,
&Jumping,
),
(With<LocalEntity>, With<InLoadedChunk>),
>,
instance_container: Res<InstanceContainer>,
) {
for (
mut physics,
direction,
position,
sprinting,
pose,
attributes,
world_name,
on_climbable,
jumping,
) in &mut query
{
let Some(world_lock) = instance_container.get(world_name) else {
continue;
};
let world = world_lock.read();
let sprinting = *sprinting.unwrap_or(&Sprinting(false));
// TODO: elytras
if physics.is_in_water() || physics.is_in_lava() {
// minecraft also checks for `this.isAffectedByFluids() &&
// !this.canStandOnFluid(fluidAtBlock)` here but it doesn't matter
// for players
travel_in_fluid(
&mut physics,
&direction,
position,
attributes,
sprinting,
on_climbable,
pose,
jumping,
&world,
);
} else {
travel_in_air(
&mut physics,
&direction,
position,
&attributes,
sprinting,
&on_climbable,
pose,
&jumping,
&world,
);
}
}
}
/// The usual movement when we're not in water or using an elytra.
fn travel_in_air(
physics: &mut Physics,
direction: &LookDirection,
position: Mut<Position>,
attributes: &Attributes,
sprinting: Sprinting,
on_climbable: &OnClimbable,
pose: Option<&Pose>,
jumping: &Jumping,
world: &Instance,
) {
let gravity = get_effective_gravity();
// LivingEntity.travel starts here
// TODO: fluids
// TODO: elytra
let block_pos_below = get_block_pos_below_that_affects_movement(&position);
let block_state_below = world
.chunks
.get_block_state(&block_pos_below)
.unwrap_or(BlockState::AIR);
let block_below: Box<dyn Block> = block_state_below.into();
let block_friction = block_below.behavior().friction;
let inertia = if physics.on_ground() {
block_friction * 0.91
} else {
0.91
};
// this applies the current delta
let mut movement = handle_relative_friction_and_calculate_movement(
HandleRelativeFrictionAndCalculateMovementOpts {
block_friction,
world: &world,
physics,
direction: &direction,
position,
attributes,
is_sprinting: *sprinting,
on_climbable,
pose,
jumping,
},
);
movement.y -= gravity;
// if (this.shouldDiscardFriction()) {
// this.setDeltaMovement(movement.x, yMovement, movement.z);
// } else {
// this.setDeltaMovement(movement.x * (double)inertia, yMovement *
// 0.9800000190734863D, movement.z * (double)inertia); }
// if should_discard_friction(self) {
if false {
physics.velocity = movement;
} else {
physics.velocity = Vec3 {
x: movement.x * inertia as f64,
y: movement.y * 0.9800000190734863f64,
z: movement.z * inertia as f64,
};
}
}
fn travel_in_fluid(
physics: &mut Physics,
direction: &LookDirection,
mut position: Mut<Position>,
attributes: &Attributes,
sprinting: Sprinting,
on_climbable: &OnClimbable,
pose: Option<&Pose>,
jumping: &Jumping,
world: &Instance,
) {
let moving_down = physics.velocity.y <= 0.;
let y = position.y;
let gravity = get_effective_gravity();
let acceleration = Vec3::new(
physics.x_acceleration as f64,
physics.y_acceleration as f64,
physics.z_acceleration as f64,
);
if physics.was_touching_water {
let mut water_movement_speed = if *sprinting { 0.9 } else { 0.8 };
let mut speed = 0.02;
let mut water_efficiency_modifier = attributes.water_movement_efficiency.calculate() as f32;
if !physics.on_ground() {
water_efficiency_modifier *= 0.5;
}
if water_efficiency_modifier > 0. {
water_movement_speed += (0.54600006 - water_movement_speed) * water_efficiency_modifier;
speed += (attributes.speed.calculate() as f32 - speed) * water_efficiency_modifier;
}
// if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) {
// waterMovementSpeed = 0.96F;
// }
move_relative(physics, direction, speed, &acceleration);
move_colliding(
MoverType::Own,
&physics.velocity.clone(),
world,
&mut position,
physics,
)
.expect("Entity should exist");
let mut new_velocity = physics.velocity;
if physics.horizontal_collision && **on_climbable {
// underwater ladders
new_velocity.y = 0.2;
}
new_velocity.x *= water_movement_speed as f64;
new_velocity.y *= 0.8;
new_velocity.z *= water_movement_speed as f64;
physics.velocity =
get_fluid_falling_adjusted_movement(gravity, moving_down, new_velocity, sprinting);
} else {
move_relative(physics, direction, 0.02, &acceleration);
move_colliding(
MoverType::Own,
&physics.velocity.clone(),
world,
&mut position,
physics,
)
.expect("Entity should exist");
if physics.lava_fluid_height <= fluid_jump_threshold() {
physics.velocity.x *= 0.5;
physics.velocity.y *= 0.8;
physics.velocity.z *= 0.5;
let new_velocity = get_fluid_falling_adjusted_movement(
gravity,
moving_down,
physics.velocity,
sprinting,
);
physics.velocity = new_velocity;
} else {
physics.velocity *= 0.5;
}
if gravity != 0.0 {
physics.velocity.y -= gravity / 4.0;
}
}
let velocity = physics.velocity;
if physics.horizontal_collision
&& is_free(
physics.bounding_box,
world,
velocity.x,
velocity.y + 0.6 - position.y + y,
velocity.z,
)
{
physics.velocity.y = 0.3;
}
}
fn get_fluid_falling_adjusted_movement(
gravity: f64,
moving_down: bool,
new_velocity: Vec3,
sprinting: Sprinting,
) -> Vec3 {
if gravity != 0. && !*sprinting {
let new_y_velocity;
if moving_down
&& (new_velocity.y - 0.005).abs() >= 0.003
&& f64::abs(new_velocity.y - gravity / 16.0) < 0.003
{
new_y_velocity = -0.003;
} else {
new_y_velocity = new_velocity.y - gravity / 16.0;
}
Vec3 {
x: new_velocity.x,
y: new_y_velocity,
z: new_velocity.z,
}
} else {
new_velocity
}
}
fn is_free(bounding_box: AABB, world: &Instance, x: f64, y: f64, z: f64) -> bool {
// let bounding_box = bounding_box.move_relative(Vec3::new(x, y, z));
let _ = (bounding_box, world, x, y, z);
// TODO: implement this, see Entity.isFree
true
}
fn get_effective_gravity() -> f64 {
// TODO: slow falling effect
0.08
}
fn fluid_jump_threshold() -> f64 {
// this is 0.0 for entities with an eye height lower than 0.4, but that's not
// implemented since it's usually not relevant for players (unless the player
// was shrunk)
0.4
}
/// applies air resistance, calls self.travel(), and some other random
/// stuff.
/// Applies air resistance and handles jumping.
///
/// Happens before [`travel::travel`].
#[allow(clippy::type_complexity)]
pub fn ai_step(
mut query: Query<
@ -400,6 +112,188 @@ pub fn ai_step(
}
}
// in minecraft, this is done as part of aiStep immediately after travel
pub fn apply_effects_from_blocks(
mut query: Query<
(
&mut Physics,
&mut LookDirection,
&mut Position,
&mut LastSentPosition,
Option<&Sprinting>,
Option<&Pose>,
&Attributes,
&InstanceName,
&OnClimbable,
&Jumping,
),
(With<LocalEntity>, With<InLoadedChunk>),
>,
instance_container: Res<InstanceContainer>,
) {
for (
mut physics,
mut look_direction,
mut position,
mut last_sent_position,
sprinting,
pose,
attributes,
world_name,
on_climbable,
jumping,
) in &mut query
{
let Some(world_lock) = instance_container.get(world_name) else {
continue;
};
let world = world_lock.read();
// if !is_affected_by_blocks {
// continue
// }
// if (this.onGround()) {
// BlockPos var3 = this.getOnPosLegacy();
// BlockState var4 = this.level().getBlockState(var3);
// var4.getBlock().stepOn(this.level(), var3, var4, this);
// }
let mut movement_this_tick = Vec::<EntityMovement>::new();
movement_this_tick.push(EntityMovement {
from: physics.old_position,
to: **position,
});
check_inside_blocks(&mut physics, &world, &movement_this_tick);
}
}
fn check_inside_blocks(
physics: &mut Physics,
world: &Instance,
movements: &[EntityMovement],
) -> Vec<BlockState> {
let mut blocks_inside = Vec::new();
let mut visited_blocks = HashSet::<BlockState>::new();
for movement in movements {
let bounding_box_at_target = physics
.dimensions
.make_bounding_box(&movement.to)
.deflate_all(1.0E-5);
for traversed_block in
box_traverse_blocks(&movement.from, &movement.to, &bounding_box_at_target)
{
// if (!this.isAlive()) {
// return;
// }
let traversed_block_state = world.get_block_state(&traversed_block).unwrap_or_default();
if traversed_block_state.is_air() {
continue;
}
if !visited_blocks.insert(traversed_block_state) {
continue;
}
/*
VoxelShape var12 = traversedBlockState.getEntityInsideCollisionShape(this.level(), traversedBlock);
if (var12 != Shapes.block() && !this.collidedWithShapeMovingFrom(from, to, traversedBlock, var12)) {
continue;
}
traversedBlockState.entityInside(this.level(), traversedBlock, this);
this.onInsideBlock(traversedBlockState);
*/
// this is different for end portal frames and tripwire hooks, i don't think it
// actually matters for a client though
let entity_inside_collision_shape = &*BLOCK_SHAPE;
if entity_inside_collision_shape != &*BLOCK_SHAPE
&& !collided_with_shape_moving_from(
&movement.from,
&movement.to,
traversed_block,
entity_inside_collision_shape,
physics,
)
{
continue;
}
handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
blocks_inside.push(traversed_block_state);
}
}
blocks_inside
}
fn collided_with_shape_moving_from(
from: &Vec3,
to: &Vec3,
traversed_block: BlockPos,
entity_inside_collision_shape: &VoxelShape,
physics: &Physics,
) -> bool {
let bounding_box_from = physics.dimensions.make_bounding_box(from);
let delta = to - from;
bounding_box_from.collided_along_vector(
delta,
&entity_inside_collision_shape
.move_relative(traversed_block.to_vec3_floored())
.to_aabbs(),
)
}
// BlockBehavior.entityInside
fn handle_entity_inside_block(
world: &Instance,
block: BlockState,
block_pos: BlockPos,
physics: &mut Physics,
) {
let registry_block = azalea_registry::Block::from(block);
match registry_block {
azalea_registry::Block::BubbleColumn => {
let block_above = world.get_block_state(&block_pos.up(1)).unwrap_or_default();
let is_block_above_empty =
block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
let drag_down = block
.property::<properties::Drag>()
.expect("drag property should always be present on bubble columns");
let velocity = &mut physics.velocity;
if is_block_above_empty {
let new_y = if drag_down {
f64::max(-0.9, velocity.y - 0.03)
} else {
f64::min(1.8, velocity.y + 0.1)
};
velocity.y = new_y;
} else {
let new_y = if drag_down {
f64::max(-0.3, velocity.y - 0.03)
} else {
f64::min(0.7, velocity.y + 0.06)
};
velocity.y = new_y;
physics.reset_fall_distance();
}
}
_ => {}
}
}
pub struct EntityMovement {
pub from: Vec3,
pub to: Vec3,
}
pub fn jump_from_ground(
physics: &mut Physics,
position: &Position,
@ -433,6 +327,12 @@ pub fn jump_from_ground(
physics.has_impulse = true;
}
pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
for (mut physics, position) in &mut query {
physics.set_old_pos(position);
}
}
fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
BlockPos::new(
position.x.floor() as i32,
@ -442,7 +342,7 @@ fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
)
}
// opts for handle_relative_friction_and_calculate_movement
/// Options for [`handle_relative_friction_and_calculate_movement`]
struct HandleRelativeFrictionAndCalculateMovementOpts<'a> {
block_friction: f32,
world: &'a Instance,
@ -455,7 +355,6 @@ struct HandleRelativeFrictionAndCalculateMovementOpts<'a> {
pose: Option<&'a Pose>,
jumping: &'a Jumping,
}
fn handle_relative_friction_and_calculate_movement(
HandleRelativeFrictionAndCalculateMovementOpts {
block_friction,
@ -619,369 +518,3 @@ fn jump_boost_power() -> f64 {
// }
0.
}
#[cfg(test)]
mod tests {
use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
use azalea_entity::{EntityBundle, EntityPlugin};
use azalea_world::{Chunk, MinecraftEntityId, PartialInstance};
use uuid::Uuid;
use super::*;
/// You need an app to spawn entities in the world and do updates.
fn make_test_app() -> App {
let mut app = App::new();
app.add_plugins((PhysicsPlugin, EntityPlugin))
.init_resource::<InstanceContainer>();
app
}
#[test]
fn test_gravity() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
// the entity has to be in a loaded chunk for physics to work
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.,
y: 70.,
z: 0.,
},
azalea_registry::EntityKind::Zombie,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// y should start at 70
assert_eq!(entity_pos.y, 70.);
}
app.update();
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// delta is applied before gravity, so the first tick only sets the delta
assert_eq!(entity_pos.y, 70.);
let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
assert!(entity_physics.velocity.y < 0.);
}
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, so now it should go down
assert!(
entity_pos.y < 70.,
"Entity y ({}) didn't go down after physics steps",
entity_pos.y
);
}
}
#[test]
fn test_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 70.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_registry::Block::Stone.into(),
&world_lock.write().chunks,
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
app.update();
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// delta will change, but it won't move until next tick
assert_eq!(entity_pos.y, 70.);
let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
assert!(entity_physics.velocity.y < 0.);
}
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, but it also does collision
assert_eq!(entity_pos.y, 70.);
}
}
#[test]
fn test_slab_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 71.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::StoneSlab {
kind: azalea_block::properties::Type::Bottom,
waterlogged: false,
}
.into(),
&world_lock.write().chunks,
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 69.5);
}
#[test]
fn test_top_slab_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 71.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::StoneSlab {
kind: azalea_block::properties::Type::Top,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.);
}
#[test]
fn test_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 73.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}
#[test]
fn test_negative_coordinates_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: -1, z: -1 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: -7.5,
y: 73.,
z: -7.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos {
x: -8,
y: 69,
z: -8,
},
azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}
}

View file

@ -0,0 +1,299 @@
use azalea_block::{Block, BlockState};
use azalea_core::{aabb::AABB, position::Vec3};
use azalea_entity::{
metadata::Sprinting, move_relative, Attributes, InLoadedChunk, Jumping, LocalEntity,
LookDirection, OnClimbable, Physics, Pose, Position,
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_ecs::prelude::*;
use crate::{
collision::{move_colliding, MoverType},
get_block_pos_below_that_affects_movement, handle_relative_friction_and_calculate_movement,
HandleRelativeFrictionAndCalculateMovementOpts,
};
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
#[allow(clippy::type_complexity)]
pub fn travel(
mut query: Query<
(
&mut Physics,
&mut LookDirection,
&mut Position,
Option<&Sprinting>,
Option<&Pose>,
&Attributes,
&InstanceName,
&OnClimbable,
&Jumping,
),
(With<LocalEntity>, With<InLoadedChunk>),
>,
instance_container: Res<InstanceContainer>,
) {
for (
mut physics,
direction,
position,
sprinting,
pose,
attributes,
world_name,
on_climbable,
jumping,
) in &mut query
{
let Some(world_lock) = instance_container.get(world_name) else {
continue;
};
let world = world_lock.read();
let sprinting = *sprinting.unwrap_or(&Sprinting(false));
// TODO: elytras
if physics.is_in_water() || physics.is_in_lava() {
// minecraft also checks for `this.isAffectedByFluids() &&
// !this.canStandOnFluid(fluidAtBlock)` here but it doesn't matter
// for players
travel_in_fluid(
&mut physics,
&direction,
position,
attributes,
sprinting,
on_climbable,
&world,
);
} else {
travel_in_air(
&mut physics,
&direction,
position,
&attributes,
sprinting,
&on_climbable,
pose,
&jumping,
&world,
);
}
}
}
/// The usual movement when we're not in water or using an elytra.
fn travel_in_air(
physics: &mut Physics,
direction: &LookDirection,
position: Mut<Position>,
attributes: &Attributes,
sprinting: Sprinting,
on_climbable: &OnClimbable,
pose: Option<&Pose>,
jumping: &Jumping,
world: &Instance,
) {
let gravity = get_effective_gravity();
let block_pos_below = get_block_pos_below_that_affects_movement(&position);
let block_state_below = world
.chunks
.get_block_state(&block_pos_below)
.unwrap_or(BlockState::AIR);
let block_below: Box<dyn Block> = block_state_below.into();
let block_friction = block_below.behavior().friction;
let inertia = if physics.on_ground() {
block_friction * 0.91
} else {
0.91
};
// this applies the current delta
let mut movement = handle_relative_friction_and_calculate_movement(
HandleRelativeFrictionAndCalculateMovementOpts {
block_friction,
world: &world,
physics,
direction: &direction,
position,
attributes,
is_sprinting: *sprinting,
on_climbable,
pose,
jumping,
},
);
movement.y -= gravity;
// if (this.shouldDiscardFriction()) {
// this.setDeltaMovement(movement.x, yMovement, movement.z);
// } else {
// this.setDeltaMovement(movement.x * (double)inertia, yMovement *
// 0.9800000190734863D, movement.z * (double)inertia); }
// if should_discard_friction(self) {
if false {
physics.velocity = movement;
} else {
physics.velocity = Vec3 {
x: movement.x * inertia as f64,
y: movement.y * 0.9800000190734863f64,
z: movement.z * inertia as f64,
};
}
}
fn travel_in_fluid(
physics: &mut Physics,
direction: &LookDirection,
mut position: Mut<Position>,
attributes: &Attributes,
sprinting: Sprinting,
on_climbable: &OnClimbable,
world: &Instance,
) {
let moving_down = physics.velocity.y <= 0.;
let y = position.y;
let gravity = get_effective_gravity();
let acceleration = Vec3::new(
physics.x_acceleration as f64,
physics.y_acceleration as f64,
physics.z_acceleration as f64,
);
if physics.was_touching_water {
let mut water_movement_speed = if *sprinting { 0.9 } else { 0.8 };
let mut speed = 0.02;
let mut water_efficiency_modifier = attributes.water_movement_efficiency.calculate() as f32;
if !physics.on_ground() {
water_efficiency_modifier *= 0.5;
}
if water_efficiency_modifier > 0. {
water_movement_speed += (0.54600006 - water_movement_speed) * water_efficiency_modifier;
speed += (attributes.speed.calculate() as f32 - speed) * water_efficiency_modifier;
}
// if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) {
// waterMovementSpeed = 0.96F;
// }
move_relative(physics, direction, speed, &acceleration);
move_colliding(
MoverType::Own,
&physics.velocity.clone(),
world,
&mut position,
physics,
)
.expect("Entity should exist");
let mut new_velocity = physics.velocity;
if physics.horizontal_collision && **on_climbable {
// underwater ladders
new_velocity.y = 0.2;
}
new_velocity.x *= water_movement_speed as f64;
new_velocity.y *= 0.8;
new_velocity.z *= water_movement_speed as f64;
physics.velocity =
get_fluid_falling_adjusted_movement(gravity, moving_down, new_velocity, sprinting);
} else {
move_relative(physics, direction, 0.02, &acceleration);
move_colliding(
MoverType::Own,
&physics.velocity.clone(),
world,
&mut position,
physics,
)
.expect("Entity should exist");
if physics.lava_fluid_height <= fluid_jump_threshold() {
physics.velocity.x *= 0.5;
physics.velocity.y *= 0.8;
physics.velocity.z *= 0.5;
let new_velocity = get_fluid_falling_adjusted_movement(
gravity,
moving_down,
physics.velocity,
sprinting,
);
physics.velocity = new_velocity;
} else {
physics.velocity *= 0.5;
}
if gravity != 0.0 {
physics.velocity.y -= gravity / 4.0;
}
}
let velocity = physics.velocity;
if physics.horizontal_collision
&& is_free(
physics.bounding_box,
world,
velocity.x,
velocity.y + 0.6 - position.y + y,
velocity.z,
)
{
physics.velocity.y = 0.3;
}
}
fn get_fluid_falling_adjusted_movement(
gravity: f64,
moving_down: bool,
new_velocity: Vec3,
sprinting: Sprinting,
) -> Vec3 {
if gravity != 0. && !*sprinting {
let new_y_velocity;
if moving_down
&& (new_velocity.y - 0.005).abs() >= 0.003
&& f64::abs(new_velocity.y - gravity / 16.0) < 0.003
{
new_y_velocity = -0.003;
} else {
new_y_velocity = new_velocity.y - gravity / 16.0;
}
Vec3 {
x: new_velocity.x,
y: new_y_velocity,
z: new_velocity.z,
}
} else {
new_velocity
}
}
fn is_free(bounding_box: AABB, world: &Instance, x: f64, y: f64, z: f64) -> bool {
// let bounding_box = bounding_box.move_relative(Vec3::new(x, y, z));
let _ = (bounding_box, world, x, y, z);
// TODO: implement this, see Entity.isFree
true
}
fn get_effective_gravity() -> f64 {
// TODO: slow falling effect
0.08
}
fn fluid_jump_threshold() -> f64 {
// this is 0.0 for entities with an eye height lower than 0.4, but that's not
// implemented since it's usually not relevant for players (unless the player
// was shrunk)
0.4
}

View file

@ -0,0 +1,365 @@
use azalea_core::{
position::{BlockPos, ChunkPos, Vec3},
resource_location::ResourceLocation,
tick::GameTick,
};
use azalea_entity::{EntityBundle, EntityPlugin, LocalEntity, Physics, Position};
use azalea_physics::PhysicsPlugin;
use azalea_world::{Chunk, InstanceContainer, MinecraftEntityId, PartialInstance};
use bevy_app::App;
use uuid::Uuid;
/// You need an app to spawn entities in the world and do updates.
fn make_test_app() -> App {
let mut app = App::new();
app.add_plugins((PhysicsPlugin, EntityPlugin))
.init_resource::<InstanceContainer>();
app
}
#[test]
fn test_gravity() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
// the entity has to be in a loaded chunk for physics to work
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.,
y: 70.,
z: 0.,
},
azalea_registry::EntityKind::Zombie,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// y should start at 70
assert_eq!(entity_pos.y, 70.);
}
app.update();
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// delta is applied before gravity, so the first tick only sets the delta
assert_eq!(entity_pos.y, 70.);
let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
assert!(entity_physics.velocity.y < 0.);
}
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, so now it should go down
assert!(
entity_pos.y < 70.,
"Entity y ({}) didn't go down after physics steps",
entity_pos.y
);
}
}
#[test]
fn test_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 70.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_registry::Block::Stone.into(),
&world_lock.write().chunks,
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
app.update();
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// delta will change, but it won't move until next tick
assert_eq!(entity_pos.y, 70.);
let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
assert!(entity_physics.velocity.y < 0.);
}
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, but it also does collision
assert_eq!(entity_pos.y, 70.);
}
}
#[test]
fn test_slab_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 71.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::StoneSlab {
kind: azalea_block::properties::Type::Bottom,
waterlogged: false,
}
.into(),
&world_lock.write().chunks,
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 69.5);
}
#[test]
fn test_top_slab_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 71.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::StoneSlab {
kind: azalea_block::properties::Type::Top,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.);
}
#[test]
fn test_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 73.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}
#[test]
fn test_negative_coordinates_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: -1, z: -1 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: -7.5,
y: 73.,
z: -7.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos {
x: -8,
y: 69,
z: -8,
},
azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}

View file

@ -1,8 +1,7 @@
use azalea_block::{Block, BlockState};
use azalea_block::{fluid_state::FluidKind, Block, BlockState};
use azalea_client::{inventory::Inventory, Client};
use azalea_entity::{FluidOnEyes, Physics};
use azalea_inventory::{components, ItemStack, Menu};
use azalea_registry::Fluid;
#[derive(Debug)]
pub struct BestToolResult {
@ -34,7 +33,12 @@ pub fn best_tool_in_hotbar_for_block(block: BlockState, menu: &Menu) -> BestTool
let mut physics = Physics::default();
physics.set_on_ground(true);
accurate_best_tool_in_hotbar_for_block(block, menu, &physics, &FluidOnEyes::new(Fluid::Empty))
accurate_best_tool_in_hotbar_for_block(
block,
menu,
&physics,
&FluidOnEyes::new(FluidKind::Empty),
)
}
pub fn accurate_best_tool_in_hotbar_for_block(