From 0d16f01571ec8315f3979eae46981e559ade1cf9 Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:45:27 -0600 Subject: [PATCH] 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 --- README.md | 2 +- azalea-block/azalea-block-macros/src/lib.rs | 10 +- azalea-block/src/behavior.rs | 9 + azalea-block/src/block_state.rs | 156 ++++ azalea-block/src/fluid_state.rs | 137 ++++ azalea-block/src/lib.rs | 241 +----- azalea-block/src/range.rs | 6 +- azalea-client/src/mining.rs | 2 +- azalea-client/src/movement.rs | 7 +- azalea-client/src/packet_handling/game.rs | 7 + azalea-core/src/aabb.rs | 376 +++++---- azalea-core/src/direction.rs | 43 +- azalea-core/src/math.rs | 19 + azalea-core/src/position.rs | 49 +- azalea-core/src/registry_holder.rs | 35 +- azalea-entity/src/attributes.rs | 1 + azalea-entity/src/dimensions.rs | 11 +- azalea-entity/src/lib.rs | 89 ++- azalea-entity/src/mining.rs | 4 +- azalea-entity/src/plugin/mod.rs | 10 +- azalea-physics/src/clip.rs | 154 +++- .../src/collision/discrete_voxel_shape.rs | 36 +- azalea-physics/src/collision/mod.rs | 44 +- azalea-physics/src/collision/shape.rs | 67 +- .../src/collision/world_collisions.rs | 29 +- azalea-physics/src/fluids.rs | 274 +++++++ azalea-physics/src/lib.rs | 731 ++++++------------ azalea-physics/src/travel.rs | 299 +++++++ azalea-physics/tests/physics.rs | 365 +++++++++ azalea-world/src/chunk_storage.rs | 3 +- azalea-world/src/container.rs | 8 +- azalea-world/src/find_blocks.rs | 2 +- azalea-world/src/palette.rs | 2 +- azalea-world/src/world.rs | 11 +- azalea/examples/testbot/commands/debug.rs | 15 +- azalea/src/auto_tool.rs | 10 +- azalea/src/pathfinder/mining.rs | 4 +- azalea/src/pathfinder/simulation.rs | 1 + 38 files changed, 2173 insertions(+), 1096 deletions(-) create mode 100644 azalea-block/src/block_state.rs create mode 100644 azalea-block/src/fluid_state.rs create mode 100644 azalea-physics/src/fluids.rs create mode 100644 azalea-physics/src/travel.rs create mode 100644 azalea-physics/tests/physics.rs diff --git a/README.md b/README.md index 992169f1..1672db69 100755 --- a/README.md +++ b/README.md @@ -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) diff --git a/azalea-block/azalea-block-macros/src/lib.rs b/azalea-block/azalea-block-macros/src/lib.rs index 9374527f..f0f27343 100755 --- a/azalea-block/azalea-block-macros/src/lib.rs +++ b/azalea-block/azalea-block-macros/src/lib.rs @@ -341,8 +341,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream { #property_enum_variants } - impl From for #property_struct_name { - fn from(value: crate::BlockStateIntegerRepr) -> Self { + impl From 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 for #property_struct_name { - fn from(value: crate::BlockStateIntegerRepr) -> Self { + impl From 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. /// diff --git a/azalea-block/src/behavior.rs b/azalea-block/src/behavior.rs index 498ba06f..aeae8a74 100755 --- a/azalea-block/src/behavior.rs +++ b/azalea-block/src/behavior.rs @@ -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, } 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 + } } diff --git a/azalea-block/src/block_state.rs b/azalea-block/src/block_state.rs new file mode 100644 index 00000000..6a185ee4 --- /dev/null +++ b/azalea-block/src/block_state.rs @@ -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 for BlockState { + type Error = (); + + /// Safely converts a u32 state id to a block state. + fn try_from(state_id: u32) -> Result { + let state_id = state_id as BlockStateIntegerRepr; + if Self::is_valid_state(state_id) { + Ok(BlockState { id: state_id }) + } else { + Err(()) + } + } +} +impl TryFrom for BlockState { + type Error = (); + + /// Safely converts a u16 state id to a block state. + fn try_from(state_id: u16) -> Result { + 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 { + 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::::from(*self) + ) + } +} + +impl From for azalea_registry::Block { + fn from(value: BlockState) -> Self { + Box::::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 = Box::::from(BlockState::AIR); + assert_eq!(block.id(), "air"); + + let block: Box = + Box::::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 + ); + } +} diff --git a/azalea-block/src/fluid_state.rs b/azalea-block/src/fluid_state.rs new file mode 100644 index 00000000..edceac05 --- /dev/null +++ b/azalea-block/src/fluid_state.rs @@ -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 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::() + .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::() + .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::() + .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 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), + }), + } + } +} diff --git a/azalea-block/src/lib.rs b/azalea-block/src/lib.rs index 4719ef42..bbf3ece7 100755 --- a/azalea-block/src/lib.rs +++ b/azalea-block/src/lib.rs @@ -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; } - -/// 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 for BlockState { - type Error = (); - - /// Safely converts a u32 state id to a block state. - fn try_from(state_id: u32) -> Result { - let state_id = state_id as BlockStateIntegerRepr; - if Self::is_valid_state(state_id) { - Ok(BlockState { id: state_id }) - } else { - Err(()) - } - } -} -impl TryFrom for BlockState { - type Error = (); - - /// Safely converts a u16 state id to a block state. - fn try_from(state_id: u16) -> Result { - 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 { - 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::::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 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::() - .unwrap_or_default() - { - Self { - fluid: azalea_registry::Fluid::Water, - amount: 8, - } - } else { - let block = Box::::from(state); - if let Some(water) = block.downcast_ref::() { - 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::() { - 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 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 for azalea_registry::Block { - fn from(value: BlockState) -> Self { - Box::::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 = Box::::from(BlockState::AIR); - assert_eq!(block.id(), "air"); - - let block: Box = - Box::::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 - ); - } -} diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index fb6552d5..7182bb65 100644 --- a/azalea-block/src/range.rs +++ b/azalea-block/src/range.rs @@ -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> for BlockStates { impl IntoIterator for BlockStates { type Item = BlockState; - type IntoIter = std::collections::hash_set::IntoIter; + type IntoIter = hash_set::IntoIter; fn into_iter(self) -> Self::IntoIter { self.set.into_iter() diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs index bf5a48ca..ac4c9c0d 100644 --- a/azalea-client/src/mining.rs +++ b/azalea-client/src/mining.rs @@ -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; diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 8801e2c6..ec5b751e 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -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 || diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs index d92f2464..0d5a6afe 100644 --- a/azalea-client/src/packet_handling/game.rs +++ b/azalea-client/src/packet_handling/game.rs @@ -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::().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::().unwrap(); + physics.set_old_pos(&position); }), }); diff --git a/azalea-core/src/aabb.rs b/azalea-core/src/aabb.rs index 70829aa2..fe45c35e 100755 --- a/azalea-core/src/aabb.rs +++ b/azalea-core/src/aabb.rs @@ -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 { 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 { + 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, 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, + delta: &Vec3, + ) -> Option { + 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, @@ -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) -> 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(¢er) { + return true; + } + + if inflated.clip(¢er, &new_center).is_some() { + return true; + } + } + + false + } +} + +impl BlockPos { + pub fn between_closed_aabb(aabb: &AABB) -> Vec { + BlockPos::between_closed(BlockPos::from(aabb.min), BlockPos::from(aabb.max)) + } + + pub fn between_closed(min: BlockPos, max: BlockPos) -> Vec { + 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.), diff --git a/azalea-core/src/direction.rs b/azalea-core/src/direction.rs index 6ff55615..d458f487 100755 --- a/azalea-core/src/direction.rs +++ b/azalea-core/src/direction.rs @@ -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, diff --git a/azalea-core/src/math.rs b/azalea-core/src/math.rs index 67ece5cc..a763fc49 100644 --- a/azalea-core/src/math.rs +++ b/azalea-core/src/math.rs @@ -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::*; diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index 819d72cf..cba58415 100755 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -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 diff --git a/azalea-core/src/registry_holder.rs b/azalea-core/src/registry_holder.rs index 8b3dd4e6..0d2588cf 100644 --- a/azalea-core/src/registry_holder.rs +++ b/azalea-core/src/registry_holder.rs @@ -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> { + 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( &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> { - 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, } diff --git a/azalea-entity/src/attributes.rs b/azalea-entity/src/attributes.rs index 797ea43c..9af3bcaa 100644 --- a/azalea-entity/src/attributes.rs +++ b/azalea-entity/src/attributes.rs @@ -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)] diff --git a/azalea-entity/src/dimensions.rs b/azalea-entity/src/dimensions.rs index 5236b80f..3db7e304 100755 --- a/azalea-entity/src/dimensions.rs +++ b/azalea-entity/src/dimensions.rs @@ -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), } } } diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 95a2ed3c..1512bbd3 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -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) } } diff --git a/azalea-entity/src/mining.rs b/azalea-entity/src/mining.rs index ee5c961b..cfa0012e 100644 --- a/azalea-entity/src/mining.rs +++ b/azalea-entity/src/mining.rs @@ -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 { diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 92918c69..7624f0ba 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -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>) { 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; } } diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index 4a374f58..b6e6a9f9 100644 --- a/azalea-physics/src/clip.rs +++ b/azalea-physics/src/clip.rs @@ -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( 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( } } } + +pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet { + 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, + 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)); + } + } + } + } +} diff --git a/azalea-physics/src/collision/discrete_voxel_shape.rs b/azalea-physics/src/collision/discrete_voxel_shape.rs index 211e6303..cacbc987 100755 --- a/azalea-physics/src/collision/discrete_voxel_shape.rs +++ b/azalea-physics/src/collision/discrete_voxel_shape.rs @@ -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; } } diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 39fc43f8..530aa47f 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -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, @@ -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::::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 +} diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index fb733cae..9d870498 100755 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -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 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, ) } } diff --git a/azalea-physics/src/collision/world_collisions.rs b/azalea-physics/src/collision/world_collisions.rs index 36488777..f0b41986 100644 --- a/azalea-physics/src/collision/world_collisions.rs +++ b/azalea-physics/src/collision/world_collisions.rs @@ -49,28 +49,19 @@ pub fn get_block_collisions(world: &Instance, aabb: AABB) -> Vec { // if it's a full block do a faster collision check if block_state.is_collision_shape_full() { if !state.aabb.intersects_aabb(&AABB { - 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); diff --git a/azalea-physics/src/fluids.rs b/azalea-physics/src/fluids.rs new file mode 100644 index 00000000..eca4266e --- /dev/null +++ b/azalea-physics/src/fluids.rs @@ -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, With), + >, + instance_container: Res, +) { + 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 +} diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index 2ca64b1f..6ea4e946 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -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, With), - >, - instance_container: Res, -) { - 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 = 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, With), + >, + instance_container: Res, +) { + 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 { + let mut blocks_inside = Vec::new(); + let mut visited_blocks = HashSet::::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::() + .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::(); - app - } - - #[test] - fn test_gravity() { - let mut app = make_test_app(); - let world_lock = app.world_mut().resource_mut::().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::(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::(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::(entity).unwrap(); - assert!(entity_physics.velocity.y < 0.); - } - app.world_mut().run_schedule(GameTick); - app.update(); - { - let entity_pos = *app.world_mut().get::(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::().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::(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::(entity).unwrap(); - assert!(entity_physics.velocity.y < 0.); - } - app.world_mut().run_schedule(GameTick); - app.update(); - { - let entity_pos = *app.world_mut().get::(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::().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::(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::().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::(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::().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::(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::().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::(entity).unwrap(); - assert_eq!(entity_pos.y, 70.5); - } -} diff --git a/azalea-physics/src/travel.rs b/azalea-physics/src/travel.rs new file mode 100644 index 00000000..08b59867 --- /dev/null +++ b/azalea-physics/src/travel.rs @@ -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, With), + >, + instance_container: Res, +) { + 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, + 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 = 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, + 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 +} diff --git a/azalea-physics/tests/physics.rs b/azalea-physics/tests/physics.rs new file mode 100644 index 00000000..c7e85006 --- /dev/null +++ b/azalea-physics/tests/physics.rs @@ -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::(); + app +} + +#[test] +fn test_gravity() { + let mut app = make_test_app(); + let world_lock = app.world_mut().resource_mut::().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::(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::(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::(entity).unwrap(); + assert!(entity_physics.velocity.y < 0.); + } + app.world_mut().run_schedule(GameTick); + app.update(); + { + let entity_pos = *app.world_mut().get::(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::().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::(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::(entity).unwrap(); + assert!(entity_physics.velocity.y < 0.); + } + app.world_mut().run_schedule(GameTick); + app.update(); + { + let entity_pos = *app.world_mut().get::(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::().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::(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::().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::(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::().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::(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::().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::(entity).unwrap(); + assert_eq!(entity_pos.y, 70.5); +} diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index 94ffb362..8362385a 100755 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -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; diff --git a/azalea-world/src/container.rs b/azalea-world/src/container.rs index 0b3a13fd..7e5927e3 100644 --- a/azalea-world/src/container.rs +++ b/azalea-world/src/container.rs @@ -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() diff --git a/azalea-world/src/find_blocks.rs b/azalea-world/src/find_blocks.rs index ff69dd2f..6228687f 100644 --- a/azalea-world/src/find_blocks.rs +++ b/azalea-world/src/find_blocks.rs @@ -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}; diff --git a/azalea-world/src/palette.rs b/azalea-world/src/palette.rs index 269e443b..31a03f3d 100755 --- a/azalea-world/src/palette.rs +++ b/azalea-world/src/palette.rs @@ -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; diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index 0a09d387..298bd598 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -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>, - /// 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, pub registries: RegistryHolder, diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index 0718bcab..1e0846f4 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -114,7 +114,20 @@ pub fn register(commands: &mut CommandDispatcher>) { 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 })), ))); diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index 768d3089..9aea23d7 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -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( diff --git a/azalea/src/pathfinder/mining.rs b/azalea/src/pathfinder/mining.rs index 62963306..8c1b2e1d 100644 --- a/azalea/src/pathfinder/mining.rs +++ b/azalea/src/pathfinder/mining.rs @@ -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; diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 630dd591..1f29ad24 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -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(), }