1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

Fluid physics (#199)

* start implementing fluid physics

* Initial implementation of fluid pushing

* different travel function in water

* bubble columns

* jumping in water

* cleanup

* change ultrawarm to be required

* fix for clippy
This commit is contained in:
mat 2025-01-10 16:45:27 -06:00 committed by GitHub
parent 615d8f9d2a
commit 0d16f01571
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 2173 additions and 1096 deletions

View file

@ -18,7 +18,7 @@ _Currently supported Minecraft version: `1.21.4`._
## Features
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity collisions and water physics aren't yet implemented)
- [Accurate physics](https://github.com/azalea-rs/azalea/blob/main/azalea-physics/src/lib.rs) (but some features like entity collisions and elytras aren't yet implemented)
- [Pathfinder](https://azalea.matdoes.dev/azalea/pathfinder/index.html)
- [Swarms](https://azalea.matdoes.dev/azalea/swarm/index.html)
- [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine)

View file

@ -341,8 +341,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#property_enum_variants
}
impl From<crate::BlockStateIntegerRepr> for #property_struct_name {
fn from(value: crate::BlockStateIntegerRepr) -> Self {
impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
match value {
#property_from_number_variants
_ => panic!("Invalid property value: {}", value),
@ -360,8 +360,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct #property_struct_name(pub bool);
impl From<crate::BlockStateIntegerRepr> for #property_struct_name {
fn from(value: crate::BlockStateIntegerRepr) -> Self {
impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
match value {
0 => Self(false),
1 => Self(true),
@ -697,7 +697,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut generated = quote! {
impl BlockState {
/// The highest possible block state ID.
pub const MAX_STATE: crate::BlockStateIntegerRepr = #last_state_id;
pub const MAX_STATE: crate::block_state::BlockStateIntegerRepr = #last_state_id;
/// Get a property from this block state. Will be `None` if the block can't have the property.
///

View file

@ -4,6 +4,8 @@ pub struct BlockBehavior {
pub destroy_time: f32,
pub explosion_resistance: f32,
pub requires_correct_tool_for_drops: bool,
pub force_solid: Option<bool>,
}
impl Default for BlockBehavior {
@ -14,6 +16,7 @@ impl Default for BlockBehavior {
destroy_time: 0.,
explosion_resistance: 0.,
requires_correct_tool_for_drops: false,
force_solid: None,
}
}
}
@ -52,4 +55,10 @@ impl BlockBehavior {
self.requires_correct_tool_for_drops = true;
self
}
// TODO: currently unused
pub fn force_solid(mut self, force_solid: bool) -> Self {
self.force_solid = Some(force_solid);
self
}
}

View file

@ -0,0 +1,156 @@
use std::{
fmt::{self, Debug},
io::{self, Cursor, Write},
};
use azalea_buf::{AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
use crate::Block;
/// The type that's used internally to represent a block state ID.
///
/// This should be either `u16` or `u32`. If you choose to modify it, you must
/// also change it in `azalea-block-macros/src/lib.rs`.
///
/// This does not affect protocol serialization, it just allows you to make the
/// internal type smaller if you want.
pub type BlockStateIntegerRepr = u16;
/// A representation of a state a block can be in.
///
/// For example, a stone block only has one state but each possible stair
/// rotation is a different state.
///
/// Note that this type is internally either a `u16` or `u32`, depending on
/// [`BlockStateIntegerRepr`].
#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)]
pub struct BlockState {
/// The protocol ID for the block state. IDs may change every
/// version, so you shouldn't hard-code them or store them in databases.
pub id: BlockStateIntegerRepr,
}
impl BlockState {
/// A shortcut for getting the air block state, since it always has an ID of
/// 0.
pub const AIR: BlockState = BlockState { id: 0 };
/// Whether the block state is possible to exist in vanilla Minecraft.
///
/// It's equivalent to checking that the state ID is not greater than
/// [`Self::MAX_STATE`].
#[inline]
pub fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
state_id <= Self::MAX_STATE
}
/// Returns true if the block is air. This only checks for normal air, not
/// other types like cave air.
#[inline]
pub fn is_air(&self) -> bool {
self == &Self::AIR
}
}
impl TryFrom<u32> for BlockState {
type Error = ();
/// Safely converts a u32 state id to a block state.
fn try_from(state_id: u32) -> Result<Self, Self::Error> {
let state_id = state_id as BlockStateIntegerRepr;
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
impl TryFrom<u16> for BlockState {
type Error = ();
/// Safely converts a u16 state id to a block state.
fn try_from(state_id: u16) -> Result<Self, Self::Error> {
let state_id = state_id as BlockStateIntegerRepr;
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
impl AzaleaRead for BlockState {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let state_id = u32::azalea_read_var(buf)?;
Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
id: state_id as i32,
})
}
}
impl AzaleaWrite for BlockState {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
u32::azalea_write_var(&(self.id as u32), buf)
}
}
impl Debug for BlockState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BlockState(id: {}, {:?})",
self.id,
Box::<dyn Block>::from(*self)
)
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn Block>::from(value).as_registry_block()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_u32() {
assert_eq!(
BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
BlockState::AIR
);
assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
}
#[test]
fn test_from_blockstate() {
let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn Block> =
Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}
#[test]
fn test_debug_blockstate() {
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::FloweringAzalea)
);
assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::BigDripleafStem)
);
assert!(
formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
"{}",
formatted
);
}
}

View file

@ -0,0 +1,137 @@
use crate::block_state::{BlockState, BlockStateIntegerRepr};
#[derive(Clone, Debug)]
pub struct FluidState {
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
/// above it, but it's usually unused by this struct.
///
/// This is different from [`crate::blocks::Water::level`], which is
/// basically the opposite (0 = full, 8 = empty). You can convert between
/// the two representations with [`to_or_from_legacy_fluid_level`].
pub amount: u8,
/// Whether this fluid is at the max level and there's another fluid of the
/// same type above it.
///
/// TODO: this is currently unused (always false), make this actually get
/// 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.kind == self.kind) || (self.amount == 0 && other.amount == 0)
}
}
impl Default for FluidState {
fn default() -> Self {
Self {
kind: FluidKind::Empty,
amount: 0,
falling: false,
}
}
}
impl From<BlockState> for FluidState {
fn from(state: BlockState) -> Self {
// note that 8 here might be treated as 9 in some cases if there's another fluid
// block of the same type above it
if state
.property::<crate::properties::Waterlogged>()
.unwrap_or_default()
{
return Self {
kind: FluidKind::Water,
amount: 8,
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 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`].
pub fn to_or_from_legacy_fluid_level(level: u8) -> u8 {
// see FlowingFluid.getLegacyLevel
8_u8.saturating_sub(level)
}
impl From<FluidState> for BlockState {
fn from(state: FluidState) -> Self {
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

@ -2,18 +2,17 @@
#![feature(trait_upcasting)]
mod behavior;
pub mod block_state;
pub mod fluid_state;
mod generated;
mod range;
use core::fmt::Debug;
use std::{
any::Any,
fmt,
io::{self, Cursor, Write},
};
use std::any::Any;
use azalea_buf::{AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
pub use behavior::BlockBehavior;
// re-exported for convenience
pub use block_state::BlockState;
pub use generated::{blocks, properties};
pub use range::BlockStates;
@ -40,233 +39,3 @@ pub trait Property {
fn try_from_block_state(state: BlockState) -> Option<Self::Value>;
}
/// The type that's used internally to represent a block state ID.
///
/// This should be either `u16` or `u32`. If you choose to modify it, you must
/// also change it in `azalea-block-macros/src/lib.rs`.
///
/// This does not affect protocol serialization, it just allows you to make the
/// internal type smaller if you want.
pub type BlockStateIntegerRepr = u16;
/// A representation of a state a block can be in.
///
/// For example, a stone block only has one state but each possible stair
/// rotation is a different state.
///
/// Note that this type is internally either a `u16` or `u32`, depending on
/// [`BlockStateIntegerRepr`].
#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)]
pub struct BlockState {
/// The protocol ID for the block state. IDs may change every
/// version, so you shouldn't hard-code them or store them in databases.
pub id: BlockStateIntegerRepr,
}
impl BlockState {
pub const AIR: BlockState = BlockState { id: 0 };
#[inline]
pub fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
state_id <= Self::MAX_STATE
}
/// Returns true if the block is air. This only checks for normal air, not
/// other types like cave air.
#[inline]
pub fn is_air(&self) -> bool {
self == &Self::AIR
}
}
impl TryFrom<u32> for BlockState {
type Error = ();
/// Safely converts a u32 state id to a block state.
fn try_from(state_id: u32) -> Result<Self, Self::Error> {
let state_id = state_id as BlockStateIntegerRepr;
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
impl TryFrom<u16> for BlockState {
type Error = ();
/// Safely converts a u16 state id to a block state.
fn try_from(state_id: u16) -> Result<Self, Self::Error> {
let state_id = state_id as BlockStateIntegerRepr;
if Self::is_valid_state(state_id) {
Ok(BlockState { id: state_id })
} else {
Err(())
}
}
}
impl AzaleaRead for BlockState {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
let state_id = u32::azalea_read_var(buf)?;
Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
id: state_id as i32,
})
}
}
impl AzaleaWrite for BlockState {
fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
u32::azalea_write_var(&(self.id as u32), buf)
}
}
impl Debug for BlockState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BlockState(id: {}, {:?})",
self.id,
Box::<dyn Block>::from(*self)
)
}
}
#[derive(Clone, Debug)]
pub struct FluidState {
pub fluid: azalea_registry::Fluid,
/// 0 = empty, 8 = full, 9 = max.
///
/// 9 is meant to be used when there's another fluid block of the same type
/// above it, but it's usually unused by this struct.
pub amount: u8,
}
impl FluidState {
/// 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.
}
}
impl Default for FluidState {
fn default() -> Self {
Self {
fluid: azalea_registry::Fluid::Empty,
amount: 0,
}
}
}
impl From<BlockState> for FluidState {
fn from(state: BlockState) -> Self {
// note that 8 here might be treated as 9 in some cases if there's another fluid
// block of the same type above it
if state
.property::<crate::properties::Waterlogged>()
.unwrap_or_default()
{
Self {
fluid: azalea_registry::Fluid::Water,
amount: 8,
}
} 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),
}
} 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),
}
} else {
Self {
fluid: azalea_registry::Fluid::Empty,
amount: 0,
}
}
}
}
}
// see FlowingFluid.getLegacyLevel
fn to_or_from_legacy_fluid_level(level: u8) -> u8 {
8_u8.saturating_sub(level)
}
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,
),
})
}
}
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn Block>::from(value).as_registry_block()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_u32() {
assert_eq!(
BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
BlockState::AIR
);
assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
}
#[test]
fn test_from_blockstate() {
let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn Block> =
Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}
#[test]
fn test_debug_blockstate() {
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::FloweringAzalea)
);
assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::BigDripleafStem)
);
assert!(
formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
"{}",
formatted
);
}
}

View file

@ -1,9 +1,9 @@
use std::{
collections::HashSet,
collections::{hash_set, HashSet},
ops::{Add, RangeInclusive},
};
use crate::{BlockState, BlockStateIntegerRepr};
use crate::{block_state::BlockStateIntegerRepr, BlockState};
#[derive(Debug, Clone)]
pub struct BlockStates {
@ -22,7 +22,7 @@ impl From<RangeInclusive<BlockStateIntegerRepr>> for BlockStates {
impl IntoIterator for BlockStates {
type Item = BlockState;
type IntoIter = std::collections::hash_set::IntoIter<BlockState>;
type IntoIter = hash_set::IntoIter<BlockState>;
fn into_iter(self) -> Self::IntoIter {
self.set.into_iter()

View file

@ -1,4 +1,4 @@
use azalea_block::{Block, BlockState, FluidState};
use azalea_block::{fluid_state::FluidState, Block, BlockState};
use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
use azalea_inventory::ItemStack;

View file

@ -65,7 +65,8 @@ impl Plugin for PlayerMovePlugin {
(tick_controls, local_player_ai_step)
.chain()
.in_set(PhysicsSet)
.before(ai_step),
.before(ai_step)
.before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
send_position.after(PhysicsSet),
)
@ -324,8 +325,8 @@ pub fn local_player_ai_step(
) {
for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
// server ai step
physics.xxa = physics_state.left_impulse;
physics.zza = physics_state.forward_impulse;
physics.x_acceleration = physics_state.left_impulse;
physics.z_acceleration = physics_state.forward_impulse;
// TODO: food data and abilities
// let has_enough_food_to_sprint = self.food_data().food_level ||

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;
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

@ -8,13 +8,8 @@ use crate::{
/// A rectangular prism with a starting and ending point.
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub struct AABB {
pub min_x: f64,
pub min_y: f64,
pub min_z: f64,
pub max_x: f64,
pub max_y: f64,
pub max_z: f64,
pub min: Vec3,
pub max: Vec3,
}
pub struct ClipPointOpts<'a> {
@ -23,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,
@ -32,51 +27,38 @@ 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_x,
min_y,
min_z,
max_x,
max_y,
max_z,
}
AABB { min, max }
}
pub fn expand_towards(&self, other: &Vec3) -> AABB {
let mut min_x = self.min_x;
let mut min_y = self.min_y;
let mut min_z = self.min_z;
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 max_x = self.max.x;
let mut max_y = self.max.y;
let mut max_z = self.max.z;
if other.x < 0.0 {
min_x += other.x;
@ -97,115 +79,93 @@ impl AABB {
}
AABB {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
}
}
pub fn inflate(&self, x: f64, y: f64, z: f64) -> AABB {
let min_x = self.min_x - x;
let min_y = self.min_y - y;
let min_z = self.min_z - z;
let min_x = self.min.x - x;
let min_y = self.min.y - y;
let min_z = self.min.z - z;
let max_x = self.max_x + x;
let max_y = self.max_y + y;
let max_z = self.max_z + z;
let max_x = self.max.x + x;
let max_y = self.max.y + y;
let max_z = self.max.z + z;
AABB {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
}
}
pub fn intersect(&self, other: &AABB) -> AABB {
let min_x = self.min_x.max(other.min_x);
let min_y = self.min_y.max(other.min_y);
let min_z = self.min_z.max(other.min_z);
let min_x = self.min.x.max(other.min.x);
let min_y = self.min.y.max(other.min.y);
let min_z = self.min.z.max(other.min.z);
let max_x = self.max_x.min(other.max_x);
let max_y = self.max_y.min(other.max_y);
let max_z = self.max_z.min(other.max_z);
let max_x = self.max.x.min(other.max.x);
let max_y = self.max.y.min(other.max.y);
let max_z = self.max.z.min(other.max.z);
AABB {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
}
}
pub fn minmax(&self, other: &AABB) -> AABB {
let min_x = self.min_x.min(other.min_x);
let min_y = self.min_y.min(other.min_y);
let min_z = self.min_z.min(other.min_z);
let min_x = self.min.x.min(other.min.x);
let min_y = self.min.y.min(other.min.y);
let min_z = self.min.z.min(other.min.z);
let max_x = self.max_x.max(other.max_x);
let max_y = self.max_y.max(other.max_y);
let max_z = self.max_z.max(other.max_z);
let max_x = self.max.x.max(other.max.x);
let max_y = self.max.y.max(other.max.y);
let max_z = self.max.z.max(other.max.z);
AABB {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
}
}
pub fn move_relative(&self, delta: &Vec3) -> AABB {
pub fn move_relative(&self, delta: Vec3) -> AABB {
AABB {
min_x: self.min_x + delta.x,
min_y: self.min_y + delta.y,
min_z: self.min_z + delta.z,
max_x: self.max_x + delta.x,
max_y: self.max_y + delta.y,
max_z: self.max_z + delta.z,
min: self.min + delta,
max: self.max + delta,
}
}
pub fn intersects_aabb(&self, other: &AABB) -> bool {
self.min_x < other.max_x
&& self.max_x > other.min_x
&& self.min_y < other.max_y
&& self.max_y > other.min_y
&& self.min_z < other.max_z
&& self.max_z > other.min_z
self.min.x < other.max.x
&& self.max.x > other.min.x
&& self.min.y < other.max.y
&& self.max.y > other.min.y
&& self.min.z < other.max.z
&& self.max.z > other.min.z
}
pub fn intersects_vec3(&self, other: &Vec3, other2: &Vec3) -> bool {
self.intersects_aabb(&AABB {
min_x: other.x.min(other2.x),
min_y: other.y.min(other2.y),
min_z: other.z.min(other2.z),
max_x: other.x.max(other2.x),
max_y: other.y.max(other2.y),
max_z: other.z.max(other2.z),
min: Vec3::new(
other.x.min(other2.x),
other.y.min(other2.y),
other.z.min(other2.z),
),
max: Vec3::new(
other.x.max(other2.x),
other.y.max(other2.y),
other.z.max(other2.z),
),
})
}
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 {
@ -217,9 +177,9 @@ impl AABB {
pub fn get_size(&self, axis: Axis) -> f64 {
axis.choose(
self.max_x - self.min_x,
self.max_y - self.min_y,
self.max_z - self.min_z,
self.max.x - self.min.x,
self.max.y - self.min.y,
self.max.z - self.min.z,
)
}
@ -227,13 +187,24 @@ impl AABB {
self.inflate(-x, -y, -z)
}
pub fn deflate_all(&mut self, amount: f64) -> AABB {
self.deflate(amount, amount, amount)
}
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,
@ -245,8 +216,8 @@ impl AABB {
let delta = to - from;
for aabb in boxes {
dir = Self::get_direction(
&aabb.move_relative(&pos.to_vec3_floored()),
dir = Self::get_direction_aabb(
&aabb.move_relative(pos.to_vec3_floored()),
from,
&mut t,
dir,
@ -264,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>,
@ -276,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,
});
@ -289,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,
});
@ -308,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,
@ -329,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,
@ -352,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,
@ -373,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,
@ -409,38 +391,96 @@ impl AABB {
}
pub fn has_nan(&self) -> bool {
self.min_x.is_nan()
|| self.min_y.is_nan()
|| self.min_z.is_nan()
|| self.max_x.is_nan()
|| self.max_y.is_nan()
|| self.max_z.is_nan()
self.min.x.is_nan()
|| self.min.y.is_nan()
|| self.min.z.is_nan()
|| self.max.x.is_nan()
|| self.max.y.is_nan()
|| self.max.z.is_nan()
}
pub fn get_center(&self) -> Vec3 {
Vec3::new(
(self.min_x + self.max_x) / 2.0,
(self.min_y + self.max_y) / 2.0,
(self.min_z + self.max_z) / 2.0,
(self.min.x + self.max.x) / 2.0,
(self.min.y + self.max.y) / 2.0,
(self.min.z + self.max.z) / 2.0,
)
}
pub fn of_size(center: Vec3, dx: f64, dy: f64, dz: f64) -> AABB {
AABB {
min_x: center.x - dx / 2.0,
min_y: center.y - dy / 2.0,
min_z: center.z - dz / 2.0,
max_x: center.x + dx / 2.0,
max_y: center.y + dy / 2.0,
max_z: center.z + dz / 2.0,
min: Vec3::new(
center.x - dx / 2.0,
center.y - dy / 2.0,
center.z - dz / 2.0,
),
max: Vec3::new(
center.x + dx / 2.0,
center.y + dy / 2.0,
center.z + dz / 2.0,
),
}
}
pub fn max(&self, axis: &Axis) -> f64 {
axis.choose(self.max_x, self.max_y, self.max_z)
axis.choose(self.max.x, self.max.y, self.max.z)
}
pub fn min(&self, axis: &Axis) -> f64 {
axis.choose(self.min_x, self.min_y, self.min_z)
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
}
}
@ -453,12 +493,8 @@ mod tests {
assert_ne!(
AABB::clip_iterable(
&vec![AABB {
min_x: 0.,
min_y: 0.,
min_z: 0.,
max_x: 1.,
max_y: 1.,
max_z: 1.,
min: Vec3::new(0., 0., 0.),
max: Vec3::new(1., 1., 1.),
}],
&Vec3::new(-1., -1., -1.),
&Vec3::new(1., 1., 1.),

View file

@ -1,6 +1,6 @@
use azalea_buf::AzBuf;
use crate::position::Vec3;
use crate::position::{BlockPos, Vec3};
#[derive(Clone, Copy, Debug, AzBuf, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@ -15,6 +15,14 @@ pub enum Direction {
}
impl Direction {
pub const HORIZONTAL: [Direction; 4] = [
Direction::North,
Direction::South,
Direction::West,
Direction::East,
];
pub const VERTICAL: [Direction; 2] = [Direction::Down, Direction::Up];
pub fn nearest(vec: Vec3) -> Direction {
let mut best_direction = Direction::North;
let mut best_direction_amount = 0.0;
@ -29,7 +37,7 @@ impl Direction {
]
.iter()
{
let amount = dir.normal().dot(vec);
let amount = dir.normal_vec3().dot(vec);
if amount > best_direction_amount {
best_direction = *dir;
best_direction_amount = amount;
@ -39,17 +47,23 @@ impl Direction {
best_direction
}
pub fn normal(self) -> Vec3 {
#[inline]
pub fn normal(self) -> BlockPos {
match self {
Direction::Down => Vec3::new(0.0, -1.0, 0.0),
Direction::Up => Vec3::new(0.0, 1.0, 0.0),
Direction::North => Vec3::new(0.0, 0.0, -1.0),
Direction::South => Vec3::new(0.0, 0.0, 1.0),
Direction::West => Vec3::new(-1.0, 0.0, 0.0),
Direction::East => Vec3::new(1.0, 0.0, 0.0),
Direction::Down => BlockPos::new(0, -1, 0),
Direction::Up => BlockPos::new(0, 1, 0),
Direction::North => BlockPos::new(0, 0, -1),
Direction::South => BlockPos::new(0, 0, 1),
Direction::West => BlockPos::new(-1, 0, 0),
Direction::East => BlockPos::new(1, 0, 0),
}
}
#[inline]
pub fn normal_vec3(self) -> Vec3 {
self.normal().to_vec3_floored()
}
pub fn opposite(self) -> Direction {
match self {
Direction::Down => Direction::Up,
@ -60,6 +74,16 @@ impl Direction {
Direction::East => Direction::West,
}
}
pub fn x(self) -> i32 {
self.normal().x
}
pub fn y(self) -> i32 {
self.normal().y
}
pub fn z(self) -> i32 {
self.normal().z
}
}
/// The four cardinal directions.
@ -75,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

@ -8,11 +8,12 @@ use std::{
fmt,
hash::Hash,
io::{Cursor, Write},
ops::{Add, AddAssign, Mul, Rem, Sub},
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub},
};
use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError};
use crate::direction::Direction;
use crate::math;
use crate::resource_location::ResourceLocation;
@ -138,7 +139,6 @@ macro_rules! vec3_impl {
}
}
}
impl Add for $name {
type Output = $name;
@ -147,6 +147,18 @@ macro_rules! vec3_impl {
(&self).add(&rhs)
}
}
impl Add<$type> for $name {
type Output = Self;
#[inline]
fn add(self, rhs: $type) -> Self::Output {
Self {
x: self.x + rhs,
y: self.y + rhs,
z: self.z + rhs,
}
}
}
impl AddAssign for $name {
#[inline]
@ -203,6 +215,35 @@ macro_rules! vec3_impl {
}
}
}
impl MulAssign<$type> for $name {
#[inline]
fn mul_assign(&mut self, multiplier: $type) {
self.x *= multiplier;
self.y *= multiplier;
self.z *= multiplier;
}
}
impl Div<$type> for $name {
type Output = Self;
#[inline]
fn div(self, divisor: $type) -> Self::Output {
Self {
x: self.x / divisor,
y: self.y / divisor,
z: self.z / divisor,
}
}
}
impl DivAssign<$type> for $name {
#[inline]
fn div_assign(&mut self, divisor: $type) {
self.x /= divisor;
self.y /= divisor;
self.z /= divisor;
}
}
impl From<($type, $type, $type)> for $name {
#[inline]
@ -345,6 +386,10 @@ impl BlockPos {
z: self.z.max(other.z),
}
}
pub fn offset_with_direction(self, direction: Direction) -> Self {
self + direction.normal()
}
}
/// Chunk coordinates are used to represent where a chunk is in the world. You

View file

@ -39,6 +39,23 @@ impl RegistryHolder {
}
}
/// Get the dimension type registry, or `None` if it doesn't exist. You
/// should do some type of error handling if this returns `None`.
pub fn dimension_type(&self) -> Option<RegistryType<DimensionTypeElement>> {
let name = ResourceLocation::new("minecraft:dimension_type");
match self.get(&name) {
Some(Ok(registry)) => Some(registry),
Some(Err(err)) => {
error!(
"Error deserializing dimension type registry: {err:?}\n{:?}",
self.map.get(&name)
);
None
}
None => None,
}
}
fn get<T: Deserialize>(
&self,
name: &ResourceLocation,
@ -66,23 +83,6 @@ impl RegistryHolder {
Some(Ok(RegistryType { map }))
}
/// Get the dimension type registry, or `None` if it doesn't exist. You
/// should do some type of error handling if this returns `None`.
pub fn dimension_type(&self) -> Option<RegistryType<DimensionTypeElement>> {
let name = ResourceLocation::new("minecraft:dimension_type");
match self.get(&name) {
Some(Ok(registry)) => Some(registry),
Some(Err(err)) => {
error!(
"Error deserializing dimension type registry: {err:?}\n{:?}",
self.map.get(&name)
);
None
}
None => None,
}
}
}
/// A collection of values for a certain type of registry data.
@ -161,6 +161,7 @@ pub struct DimensionTypeElement {
pub struct DimensionTypeElement {
pub height: u32,
pub min_y: i32,
pub ultrawarm: bool,
#[simdnbt(flatten)]
pub _extra: HashMap<String, NbtTag>,
}

View file

@ -11,6 +11,7 @@ use thiserror::Error;
pub struct Attributes {
pub speed: AttributeInstance,
pub attack_speed: AttributeInstance,
pub water_movement_efficiency: AttributeInstance,
}
#[derive(Clone, Debug)]

View file

@ -7,17 +7,12 @@ 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 {
min_x: pos.x - radius,
min_y: pos.y,
min_z: pos.z - radius,
max_x: pos.x + radius,
max_y: pos.y + height,
max_z: pos.z + radius,
min: Vec3::new(pos.x - radius, pos.y, pos.z - radius),
max: Vec3::new(pos.x + radius, pos.y + height, pos.z + radius),
}
}
}

View file

@ -17,7 +17,7 @@ use std::{
};
pub use attributes::Attributes;
use azalea_block::BlockState;
use azalea_block::{fluid_state::FluidKind, BlockState};
use azalea_buf::AzBuf;
use azalea_core::{
aabb::AABB,
@ -207,8 +207,8 @@ impl From<&LastSentPosition> for BlockPos {
/// A component for entities that can jump.
///
/// If this is true, the entity will try to jump every tick. (It's equivalent to
/// the space key being held in vanilla.)
/// If this is true, the entity will try to jump every tick. It's equivalent to
/// the space key being held in vanilla.
#[derive(Debug, Component, Copy, Clone, Deref, DerefMut, Default)]
pub struct Jumping(bool);
@ -251,19 +251,33 @@ impl Eq for LookDirection {}
#[derive(Debug, Component, Clone, Default)]
pub struct Physics {
/// How fast the entity is moving.
///
/// Sometimes referred to as the delta movement.
pub velocity: Vec3,
pub vec_delta_codec: VecDeltaCodec,
/// X acceleration.
pub xxa: f32,
/// Y acceleration.
pub yya: f32,
/// Z acceleration.
pub zza: f32,
/// 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.
///
/// You should typically not set this yourself, since it's controlled by how
/// the entity is trying to move.
pub x_acceleration: f32,
pub y_acceleration: f32,
pub z_acceleration: f32,
on_ground: bool,
last_on_ground: bool,
pub vec_delta_codec: VecDeltaCodec,
/// The number of ticks until we jump again, if the jump key is being held.
///
/// This must be 0 for us to be able to jump. Sets to 10 when we do a jump
/// and sets to 0 if we're not trying to jump.
pub no_jump_delay: u32,
/// The width and height of the entity.
pub dimensions: EntityDimensions,
@ -276,21 +290,35 @@ pub struct Physics {
pub horizontal_collision: bool,
// pub minor_horizontal_collision: bool,
pub vertical_collision: bool,
pub water_fluid_height: f64,
pub lava_fluid_height: f64,
pub was_touching_water: bool,
// TODO: implement fall_distance
pub fall_distance: f32,
// TODO: implement remaining_fire_ticks
pub remaining_fire_ticks: i32,
}
impl Physics {
pub fn new(dimensions: EntityDimensions, pos: Vec3) -> Self {
Self {
velocity: Vec3::default(),
vec_delta_codec: VecDeltaCodec::new(pos),
xxa: 0.,
yya: 0.,
zza: 0.,
old_position: pos,
x_acceleration: 0.,
y_acceleration: 0.,
z_acceleration: 0.,
on_ground: false,
last_on_ground: false,
bounding_box: dimensions.make_bounding_box(pos),
no_jump_delay: 0,
bounding_box: dimensions.make_bounding_box(&pos),
dimensions,
has_impulse: false,
@ -298,7 +326,12 @@ impl Physics {
horizontal_collision: false,
vertical_collision: false,
vec_delta_codec: VecDeltaCodec::new(pos),
water_fluid_height: 0.,
lava_fluid_height: 0.,
was_touching_water: false,
fall_distance: 0.,
remaining_fire_ticks: 0,
}
}
@ -321,6 +354,25 @@ impl Physics {
pub fn set_last_on_ground(&mut self, last_on_ground: bool) {
self.last_on_ground = last_on_ground;
}
pub fn reset_fall_distance(&mut self) {
self.fall_distance = 0.;
}
pub fn clear_fire(&mut self) {
self.remaining_fire_ticks = 0;
}
pub fn is_in_water(&self) -> bool {
self.was_touching_water
}
pub fn is_in_lava(&self) -> bool {
// 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.
@ -420,10 +472,11 @@ impl EntityBundle {
// entities have different defaults
speed: AttributeInstance::new(0.1),
attack_speed: AttributeInstance::new(4.0),
water_movement_efficiency: AttributeInstance::new(0.0),
},
jumping: Jumping(false),
fluid_on_eyes: FluidOnEyes(azalea_registry::Fluid::Empty),
fluid_on_eyes: FluidOnEyes(FluidKind::Empty),
on_climbable: OnClimbable(false),
}
}
@ -444,10 +497,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};
@ -104,11 +104,11 @@ pub fn update_fluid_on_eyes(
.read()
.get_fluid_state(&eye_block_pos)
.unwrap_or_default();
let fluid_cutoff_y = eye_block_pos.y as f64 + (fluid_at_eye.amount as f64 / 16f64);
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::{BlockState, FluidState};
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

@ -238,39 +238,33 @@ impl BitSetDiscreteVoxelShape {
var2: bool,
) {
let mut var3 = BitSetDiscreteVoxelShape::from(var0);
for var4 in 0..var3.y_size {
for var5 in 0..var3.x_size {
for y in 0..var3.y_size {
for x in 0..var3.x_size {
let mut var6 = None;
for var7 in 0..=var3.z_size {
if var3.is_full_wide(var5, var4, var7) {
for z in 0..=var3.z_size {
if var3.is_full_wide(x, y, z) {
if var2 {
if var6.is_none() {
var6 = Some(var7);
var6 = Some(z);
}
} else {
consumer(var5, var4, var7, var5 + 1, var4 + 1, var7 + 1);
consumer(x, y, z, x + 1, y + 1, z + 1);
}
} else if var6.is_some() {
let mut var8 = var5;
let mut var9 = var4;
var3.clear_z_strip(var6.unwrap(), var7, var5, var4);
while var3.is_z_strip_full(var6.unwrap(), var7, var8 + 1, var4) {
var3.clear_z_strip(var6.unwrap(), var7, var8 + 1, var4);
let mut var8 = x;
let mut var9 = y;
var3.clear_z_strip(var6.unwrap(), z, x, y);
while var3.is_z_strip_full(var6.unwrap(), z, var8 + 1, y) {
var3.clear_z_strip(var6.unwrap(), z, var8 + 1, y);
var8 += 1;
}
while var3.is_xz_rectangle_full(
var5,
var8 + 1,
var6.unwrap(),
var7,
var9 + 1,
) {
for var10 in var5..=var8 {
var3.clear_z_strip(var6.unwrap(), var7, var10, var9 + 1);
while var3.is_xz_rectangle_full(x, var8 + 1, var6.unwrap(), z, var9 + 1) {
for var10 in x..=var8 {
var3.clear_z_strip(var6.unwrap(), z, var10, var9 + 1);
}
var9 += 1;
}
consumer(var5, var4, var6.unwrap(), var8 + 1, var9 + 1, var7);
consumer(x, y, var6.unwrap(), var8 + 1, var9 + 1, z);
var6 = None;
}
}

View file

@ -6,7 +6,7 @@ mod world_collisions;
use std::{ops::Add, sync::LazyLock};
use azalea_block::FluidState;
use azalea_block::{fluid_state::FluidState, BlockState};
use azalea_core::{
aabb::AABB,
direction::Axis,
@ -22,6 +22,7 @@ use tracing::warn;
use self::world_collisions::get_block_collisions;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MoverType {
Own,
Player,
@ -111,7 +112,7 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics)
y: 0.,
z: movement.z,
},
&entity_bounding_box.move_relative(&directly_up_delta),
&entity_bounding_box.move_relative(directly_up_delta),
world,
entity_collisions.clone(),
)
@ -132,7 +133,7 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics)
y: -step_to_delta.y + movement.y,
z: 0.,
},
&entity_bounding_box.move_relative(&step_to_delta),
&entity_bounding_box.move_relative(step_to_delta),
world,
entity_collisions.clone(),
));
@ -143,8 +144,10 @@ fn collide(movement: &Vec3, world: &Instance, physics: &azalea_entity::Physics)
}
/// Move an entity by a given delta, checking for collisions.
///
/// In Mojmap, this is `Entity.move`.
pub fn move_colliding(
_mover_type: &MoverType,
_mover_type: MoverType,
movement: &Vec3,
world: &Instance,
position: &mut Mut<azalea_entity::Position>,
@ -296,7 +299,7 @@ fn collide_with_shapes(
if y_movement != 0. {
y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement);
if y_movement != 0. {
entity_box = entity_box.move_relative(&Vec3 {
entity_box = entity_box.move_relative(Vec3 {
x: 0.,
y: y_movement,
z: 0.,
@ -311,7 +314,7 @@ fn collide_with_shapes(
if more_z_movement && z_movement != 0. {
z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement);
if z_movement != 0. {
entity_box = entity_box.move_relative(&Vec3 {
entity_box = entity_box.move_relative(Vec3 {
x: 0.,
y: 0.,
z: z_movement,
@ -322,7 +325,7 @@ fn collide_with_shapes(
if x_movement != 0. {
x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement);
if x_movement != 0. {
entity_box = entity_box.move_relative(&Vec3 {
entity_box = entity_box.move_relative(Vec3 {
x: x_movement,
y: 0.,
z: 0.,
@ -352,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;
}
}
@ -384,3 +387,28 @@ pub fn fluid_shape(
fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
}
/// Whether the block is treated as "motion blocking".
///
/// This is marked as deprecated in Minecraft.
pub fn legacy_blocks_motion(block: BlockState) -> bool {
let registry_block = azalea_registry::Block::from(block);
legacy_calculate_solid(block)
&& registry_block != azalea_registry::Block::Cobweb
&& registry_block != azalea_registry::Block::BambooSapling
}
pub fn legacy_calculate_solid(block: BlockState) -> bool {
// force_solid has to be checked before anything else
let block_trait = Box::<dyn azalea_block::Block>::from(block);
if let Some(solid) = block_trait.behavior().force_solid {
return solid;
}
let shape = block.collision_shape();
if shape.is_empty() {
return false;
}
let bounds = shape.bounds();
bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
}

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(),
))
}
@ -526,13 +535,6 @@ impl VoxelShape {
movement
}
// public VoxelShape optimize() {
// VoxelShape[] var1 = new VoxelShape[]{Shapes.empty()};
// this.forAllBoxes((var1x, var3, var5, var7, var9, var11) -> {
// var1[0] = Shapes.joinUnoptimized(var1[0], Shapes.box(var1x, var3,
// var5, var7, var9, var11), BooleanOp.OR); });
// return var1[0];
// }
fn optimize(&self) -> VoxelShape {
let mut shape = EMPTY_SHAPE.clone();
self.for_all_boxes(|var1x, var3, var5, var7, var9, var11| {
@ -545,35 +547,10 @@ impl VoxelShape {
shape
}
// public void forAllBoxes(Shapes.DoubleLineConsumer var1) {
// DoubleList var2 = this.getCoords(Direction.Axis.X);
// DoubleList var3 = this.getCoords(Direction.Axis.Y);
// DoubleList var4 = this.getCoords(Direction.Axis.Z);
// this.shape.forAllBoxes((var4x, var5, var6, var7, var8, var9) -> {
// var1.consume(var2.getDouble(var4x), var3.getDouble(var5),
// var4.getDouble(var6), var2.getDouble(var7), var3.getDouble(var8),
// var4.getDouble(var9)); }, true);
// }
pub fn for_all_boxes(&self, mut consumer: impl FnMut(f64, f64, f64, f64, f64, f64))
where
Self: Sized,
{
// let x_coords = self.get_coords(Axis::X);
// let y_coords = self.get_coords(Axis::Y);
// let z_coords = self.get_coords(Axis::Z);
// self.shape().for_all_boxes(
// |var4x, var5, var6, var7, var8, var9| {
// consumer(
// x_coords[var4x as usize],
// y_coords[var5 as usize],
// z_coords[var6 as usize],
// x_coords[var7 as usize],
// y_coords[var8 as usize],
// z_coords[var9 as usize],
// )
// },
// true,
// );
let x_coords = self.get_coords(Axis::X);
let y_coords = self.get_coords(Axis::Y);
let z_coords = self.get_coords(Axis::Z);
@ -596,22 +573,26 @@ impl VoxelShape {
let mut aabbs = Vec::new();
self.for_all_boxes(|min_x, min_y, min_z, max_x, max_y, max_z| {
aabbs.push(AABB {
min_x,
min_y,
min_z,
max_x,
max_y,
max_z,
min: Vec3::new(min_x, min_y, min_z),
max: Vec3::new(max_x, max_y, max_z),
});
});
aabbs
}
pub fn bounds(&self) -> AABB {
assert!(!self.is_empty(), "Can't get bounds for empty shape");
AABB {
min: Vec3::new(self.min(Axis::X), self.min(Axis::Y), self.min(Axis::Z)),
max: Vec3::new(self.max(Axis::X), self.max(Axis::Y), self.max(Axis::Z)),
}
}
}
impl From<AABB> for VoxelShape {
fn from(aabb: AABB) -> Self {
box_shape_unchecked(
aabb.min_x, aabb.min_y, aabb.min_z, aabb.max_x, aabb.max_y, aabb.max_z,
aabb.min.x, aabb.min.y, aabb.min.z, aabb.max.x, aabb.max.y, aabb.max.z,
)
}
}

View file

@ -49,28 +49,19 @@ 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 {
min_x: item.pos.x as f64,
min_y: item.pos.y as f64,
min_z: item.pos.z as f64,
max_x: (item.pos.x + 1) as f64,
max_y: (item.pos.y + 1) as f64,
max_z: (item.pos.z + 1) as f64,
min: item.pos.to_vec3_floored(),
max: (item.pos + 1).to_vec3_floored(),
}) {
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;
@ -95,15 +86,15 @@ pub struct BlockCollisionsState<'a> {
impl<'a> BlockCollisionsState<'a> {
pub fn new(world: &'a Instance, aabb: AABB) -> Self {
let origin = BlockPos {
x: (aabb.min_x - EPSILON).floor() as i32 - 1,
y: (aabb.min_y - EPSILON).floor() as i32 - 1,
z: (aabb.min_z - EPSILON).floor() as i32 - 1,
x: (aabb.min.x - EPSILON).floor() as i32 - 1,
y: (aabb.min.y - EPSILON).floor() as i32 - 1,
z: (aabb.min.z - EPSILON).floor() as i32 - 1,
};
let end = BlockPos {
x: (aabb.max_x + EPSILON).floor() as i32 + 1,
y: (aabb.max_y + EPSILON).floor() as i32 + 1,
z: (aabb.max_z + EPSILON).floor() as i32 + 1,
x: (aabb.max.x + EPSILON).floor() as i32 + 1,
y: (aabb.max.y + EPSILON).floor() as i32 + 1,
z: (aabb.max.z + EPSILON).floor() as i32 + 1,
};
let cursor = Cursor3d::new(origin, end);

View file

@ -0,0 +1,274 @@
use azalea_block::{
fluid_state::{FluidKind, FluidState},
BlockState,
};
use azalea_core::{
direction::Direction,
position::{BlockPos, Vec3},
};
use azalea_entity::{InLoadedChunk, LocalEntity, Physics, Position};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_ecs::prelude::*;
use crate::collision::legacy_blocks_motion;
#[allow(clippy::type_complexity)]
pub fn update_in_water_state_and_do_fluid_pushing(
mut query: Query<
(&mut Physics, &Position, &InstanceName),
(With<LocalEntity>, With<InLoadedChunk>),
>,
instance_container: Res<InstanceContainer>,
) {
for (mut physics, position, instance_name) in &mut query {
let world_lock = instance_container
.get(instance_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
physics.water_fluid_height = 0.;
physics.lava_fluid_height = 0.;
update_in_water_state_and_do_water_current_pushing(&mut physics, &world, position);
let is_ultrawarm = world
.registries
.dimension_type()
.and_then(|d| d.map.get(instance_name).map(|d| d.ultrawarm))
== Some(true);
let lava_push_factor = if is_ultrawarm {
0.007
} else {
0.0023333333333333335
};
update_fluid_height_and_do_fluid_pushing(
&mut physics,
&world,
FluidKind::Lava,
lava_push_factor,
);
}
}
fn update_in_water_state_and_do_water_current_pushing(
physics: &mut Physics,
world: &Instance,
_position: &Position,
) {
// TODO: implement vehicles and boats
// if vehicle == AbstractBoat {
// if !boat.is_underwater() {
// *was_touching_water = false;
// }
// }
// updateFluidHeightAndDoFluidPushing
if update_fluid_height_and_do_fluid_pushing(physics, world, FluidKind::Water, 0.014) {
// if !was_touching_water && !first_tick {
// do_water_splash_effect();
// }
physics.reset_fall_distance();
physics.was_touching_water = true;
physics.clear_fire();
} else {
physics.was_touching_water = false;
}
}
fn update_fluid_height_and_do_fluid_pushing(
physics: &mut Physics,
world: &Instance,
checking_fluid: FluidKind,
fluid_push_factor: f64,
) -> bool {
// if touching_unloaded_chunk() {
// return false;
// }
let checking_liquids_aabb = physics.bounding_box.deflate_all(0.001);
let min_x = checking_liquids_aabb.min.x.floor() as i32;
let min_y = checking_liquids_aabb.min.y.floor() as i32;
let min_z = checking_liquids_aabb.min.z.floor() as i32;
let max_x = checking_liquids_aabb.max.x.ceil() as i32;
let max_y = checking_liquids_aabb.max.y.ceil() as i32;
let max_z = checking_liquids_aabb.max.z.ceil() as i32;
let mut min_height_touching = 0.;
let is_entity_pushable_by_fluid = true;
let mut touching_fluid = false;
let mut additional_player_delta = Vec3::default();
let mut num_fluids_being_touched = 0;
for cur_x in min_x..=max_x {
for cur_y in min_y..=max_y {
for cur_z in min_z..=max_z {
let cur_pos = BlockPos::new(cur_x, cur_y, cur_z);
let Some(fluid_at_cur_pos) = world.get_fluid_state(&cur_pos) else {
continue;
};
if fluid_at_cur_pos.kind != checking_fluid {
continue;
}
let fluid_max_y = (cur_y as f32 + fluid_at_cur_pos.height()) as f64;
if fluid_max_y < checking_liquids_aabb.min.y {
continue;
}
touching_fluid = true;
min_height_touching = f64::max(
fluid_max_y - checking_liquids_aabb.min.y,
min_height_touching,
);
if !is_entity_pushable_by_fluid {
continue;
}
let mut additional_player_delta_for_fluid =
get_fluid_flow(&fluid_at_cur_pos, world, cur_pos);
if min_height_touching < 0.4 {
additional_player_delta_for_fluid *= min_height_touching;
};
additional_player_delta += additional_player_delta_for_fluid;
num_fluids_being_touched += 1;
}
}
}
if additional_player_delta.length() > 0. {
additional_player_delta /= num_fluids_being_touched as f64;
// if entity_kind != EntityKind::Player {
// additional_player_delta = additional_player_delta.normalize();
// }
let player_delta = physics.velocity;
additional_player_delta *= fluid_push_factor;
const MIN_PUSH: f64 = 0.003;
const MIN_PUSH_LENGTH: f64 = MIN_PUSH * 1.5;
if player_delta.x.abs() < MIN_PUSH
&& player_delta.z.abs() < MIN_PUSH
&& additional_player_delta.length() < MIN_PUSH_LENGTH
{
additional_player_delta = additional_player_delta.normalize() * MIN_PUSH_LENGTH;
}
physics.velocity += additional_player_delta;
}
match 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
}
pub fn update_swimming() {
// TODO: swimming
}
// FlowingFluid.getFlow
pub fn get_fluid_flow(fluid: &FluidState, world: &Instance, pos: BlockPos) -> Vec3 {
let mut z_flow: f64 = 0.;
let mut x_flow: f64 = 0.;
for direction in Direction::HORIZONTAL {
let adjacent_block_pos = pos.offset_with_direction(direction);
let adjacent_fluid_state = world
.get_fluid_state(&adjacent_block_pos)
.unwrap_or_default();
if fluid.affects_flow(&adjacent_fluid_state) {
let mut adjacent_fluid_height = adjacent_fluid_state.height();
let mut adjacent_height_difference: f32 = 0.;
if adjacent_fluid_height == 0. {
if !legacy_blocks_motion(
world
.get_block_state(&adjacent_block_pos)
.unwrap_or_default(),
) {
let block_pos_below_adjacent = adjacent_block_pos.down(1);
let fluid_below_adjacent = world
.get_fluid_state(&block_pos_below_adjacent)
.unwrap_or_default();
if fluid.affects_flow(&fluid_below_adjacent) {
adjacent_fluid_height = fluid_below_adjacent.height();
if adjacent_fluid_height > 0. {
adjacent_height_difference =
fluid.height() - (adjacent_fluid_height - 0.8888889);
}
}
}
} else if adjacent_fluid_height > 0. {
adjacent_height_difference = fluid.height() - adjacent_fluid_height;
}
if adjacent_height_difference != 0. {
x_flow += (direction.x() as f32 * adjacent_height_difference) as f64;
z_flow += (direction.z() as f32 * adjacent_height_difference) as f64;
}
}
}
let mut flow = Vec3::new(x_flow, 0., z_flow);
if fluid.falling {
for direction in Direction::HORIZONTAL {
let adjacent_block_pos = pos.offset_with_direction(direction);
if is_solid_face(fluid, world, adjacent_block_pos, direction)
|| is_solid_face(fluid, world, adjacent_block_pos.up(1), direction)
{
flow = flow.normalize() + Vec3::new(0., -6., 0.);
break;
}
}
}
flow.normalize()
}
// i don't really get what this is for
fn is_solid_face(
fluid: &FluidState,
world: &Instance,
adjacent_pos: BlockPos,
direction: Direction,
) -> bool {
let block_state = world.get_block_state(&adjacent_pos).unwrap_or_default();
let fluid_state = world.get_fluid_state(&adjacent_pos).unwrap_or_default();
if fluid_state.is_same_kind(fluid) {
return false;
}
if direction == Direction::Up {
return true;
}
let registry_block = azalea_registry::Block::from(block_state);
if matches!(
registry_block,
// frosted ice is from frost walker
azalea_registry::Block::Ice | azalea_registry::Block::FrostedIce
) {
return false;
}
is_face_sturdy(block_state, world, adjacent_pos, direction)
}
fn is_face_sturdy(
_block_state: BlockState,
_world: &Instance,
_pos: BlockPos,
_direction: Direction,
) -> bool {
// TODO: this does a whole bunch of physics shape checks for waterlogged blocks
// that i honestly cannot be bothered to implement right now
// see BlockBehavior.isFaceSturdy in the decompiled minecraft source
// also, this probably should be in a module other than fluids.rs
false
}

View file

@ -3,8 +3,12 @@
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::{
math,
position::{BlockPos, Vec3},
@ -22,7 +26,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)]
@ -33,7 +38,15 @@ impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
GameTick,
(ai_step, travel)
(
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,
apply_effects_from_blocks,
)
.chain()
.in_set(PhysicsSet)
.after(azalea_entity::update_in_loaded_chunk),
@ -41,110 +54,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();
// if !self.is_effective_ai() && !self.is_controlled_by_local_instance() {
// // this.calculateEntityAnimation(this, this instanceof FlyingAnimal);
// return;
// }
let gravity: f64 = 0.08;
// TODO: slow falling effect
// let is_falling = self.delta.y <= 0.;
// 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: &mut physics,
direction: &direction,
position,
attributes,
is_sprinting: sprinting.map(|s| **s).unwrap_or(false),
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,
};
}
}
}
/// 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<
@ -164,6 +76,10 @@ pub fn ai_step(
// vanilla does movement interpolation here, doesn't really matter much for a
// bot though
if physics.no_jump_delay > 0 {
physics.no_jump_delay -= 1;
}
if physics.velocity.x.abs() < 0.003 {
physics.velocity.x = 0.;
}
@ -178,27 +94,221 @@ pub fn ai_step(
if **jumping {
// TODO: jumping in liquids and jump delay
if physics.on_ground() {
jump_from_ground(
&mut physics,
position,
look_direction,
sprinting,
instance_name,
&instance_container,
)
let fluid_height = if physics.is_in_lava() {
physics.lava_fluid_height
} else if physics.is_in_water() {
physics.water_fluid_height
} else {
0.
};
let in_water = physics.is_in_water() && fluid_height > 0.;
let fluid_jump_threshold = travel::fluid_jump_threshold();
if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
if !physics.is_in_lava()
|| physics.on_ground() && fluid_height <= fluid_jump_threshold
{
if physics.on_ground()
|| in_water
&& fluid_height <= fluid_jump_threshold
&& physics.no_jump_delay == 0
{
jump_from_ground(
&mut physics,
position,
look_direction,
sprinting,
instance_name,
&instance_container,
);
physics.no_jump_delay = 10;
}
} else {
jump_in_liquid(&mut physics);
}
} else {
jump_in_liquid(&mut physics);
}
}
} else {
physics.no_jump_delay = 0;
}
physics.xxa *= 0.98;
physics.zza *= 0.98;
physics.x_acceleration *= 0.98;
physics.z_acceleration *= 0.98;
// TODO: freezing, pushEntities, drowning damage (in their own systems,
// after `travel`)
}
}
fn jump_in_liquid(physics: &mut Physics) {
physics.velocity.y += 0.04;
}
// in minecraft, this is done as part of aiStep immediately after travel
#[allow(clippy::type_complexity)]
pub fn apply_effects_from_blocks(
mut query: Query<
(&mut Physics, &Position, &InstanceName),
(With<LocalEntity>, With<InLoadedChunk>),
>,
instance_container: Res<InstanceContainer>,
) {
for (mut physics, position, world_name) 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);
// }
// minecraft adds more entries to the list when the code is running on the
// server
let movement_this_tick = [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);
#[allow(clippy::single_match)]
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,
@ -232,6 +342,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,
@ -241,7 +357,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,
@ -254,7 +370,6 @@ struct HandleRelativeFrictionAndCalculateMovementOpts<'a> {
pose: Option<&'a Pose>,
jumping: &'a Jumping,
}
fn handle_relative_friction_and_calculate_movement(
HandleRelativeFrictionAndCalculateMovementOpts {
block_friction,
@ -274,22 +389,22 @@ fn handle_relative_friction_and_calculate_movement(
direction,
get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting),
&Vec3 {
x: physics.xxa as f64,
y: physics.yya as f64,
z: physics.zza as f64,
x: physics.x_acceleration as f64,
y: physics.y_acceleration as f64,
z: physics.z_acceleration as f64,
},
);
physics.velocity = handle_on_climbable(physics.velocity, on_climbable, &position, world, pose);
move_colliding(
&MoverType::Own,
MoverType::Own,
&physics.velocity.clone(),
world,
&mut position,
physics,
)
.expect("Entity should exist.");
.expect("Entity should exist");
// let delta_movement = entity.delta;
// ladders
// if ((entity.horizontalCollision || entity.jumping) && (entity.onClimbable()
@ -418,369 +533,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.
#[allow(clippy::too_many_arguments)]
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,
physics,
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
{
-0.003
} else {
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
}
pub 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

@ -7,7 +7,8 @@ use std::{
sync::{Arc, Weak},
};
use azalea_block::{BlockState, BlockStateIntegerRepr, FluidState};
use azalea_block::block_state::{BlockState, BlockStateIntegerRepr};
use azalea_block::fluid_state::FluidState;
use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
use azalea_core::position::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos};
use nohash_hasher::IntMap;

View file

@ -56,14 +56,14 @@ impl InstanceContainer {
let existing = existing_lock.read();
if existing.chunks.height != height {
error!(
"Shared dimension height mismatch: {} != {}",
existing.chunks.height, height,
"Shared dimension height mismatch: {} != {height}",
existing.chunks.height
);
}
if existing.chunks.min_y != min_y {
error!(
"Shared world min_y mismatch: {} != {}",
existing.chunks.min_y, min_y,
"Shared world min_y mismatch: {} != {min_y}",
existing.chunks.min_y
);
}
existing_lock.clone()

View file

@ -1,4 +1,4 @@
use azalea_block::{BlockState, BlockStates};
use azalea_block::{block_state::BlockState, BlockStates};
use azalea_core::position::{BlockPos, ChunkPos};
use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, Instance};

View file

@ -1,6 +1,6 @@
use std::io::{Cursor, Write};
use azalea_block::BlockStateIntegerRepr;
use azalea_block::block_state::BlockStateIntegerRepr;
use azalea_buf::{AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError};
use azalea_core::math;
use tracing::warn;

View file

@ -5,7 +5,8 @@ use std::{
fmt::Debug,
};
use azalea_block::{BlockState, FluidState};
use azalea_block::fluid_state::FluidState;
use azalea_block::BlockState;
use azalea_core::position::{BlockPos, ChunkPos};
use azalea_core::registry_holder::RegistryHolder;
use bevy_ecs::{component::Component, entity::Entity};
@ -88,8 +89,12 @@ pub struct Instance {
/// An index of all the entities we know are in the chunks of the world
pub entities_by_chunk: HashMap<ChunkPos, HashSet<Entity>>,
/// An index of Minecraft entity IDs to Azalea ECS entities. You should
/// avoid using this and instead use `azalea_entity::EntityIdIndex`
/// An index of Minecraft entity IDs to Azalea ECS entities.
///
/// You should avoid using this (particularly if you're using swarms) and
/// instead use `azalea_entity::EntityIdIndex`, since some servers may
/// give different entity IDs for the same entities to different
/// players.
pub entity_by_id: IntMap<MinecraftEntityId, Entity>,
pub registries: RegistryHolder,

View file

@ -114,7 +114,20 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
println!("getblock xyz {x} {y} {z}");
let block_pos = BlockPos::new(x, y, z);
let block = source.bot.world().read().get_block_state(&block_pos);
source.reply(&format!("Block at {block_pos:?} is {block:?}"));
source.reply(&format!("Block at {block_pos} is {block:?}"));
1
})),
)));
commands.register(literal("getfluid").then(argument("x", integer()).then(
argument("y", integer()).then(argument("z", integer()).executes(|ctx: &Ctx| {
let source = ctx.source.lock();
let x = get_integer(ctx, "x").unwrap();
let y = get_integer(ctx, "y").unwrap();
let z = get_integer(ctx, "z").unwrap();
println!("getfluid xyz {x} {y} {z}");
let block_pos = BlockPos::new(x, y, z);
let block = source.bot.world().read().get_fluid_state(&block_pos);
source.reply(&format!("Fluid at {block_pos} is {block:?}"));
1
})),
)));

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(

View file

@ -1,6 +1,8 @@
use std::{cell::UnsafeCell, ops::RangeInclusive};
use azalea_block::{properties::Waterlogged, BlockState, BlockStateIntegerRepr, BlockStates};
use azalea_block::{
block_state::BlockStateIntegerRepr, properties::Waterlogged, BlockState, BlockStates,
};
use azalea_inventory::Menu;
use nohash_hasher::IntMap;

View file

@ -38,6 +38,7 @@ impl SimulatedPlayerBundle {
attributes: Attributes {
speed: AttributeInstance::new(0.1),
attack_speed: AttributeInstance::new(4.0),
water_movement_efficiency: AttributeInstance::new(0.0),
},
inventory: Inventory::default(),
}