aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-01-10 16:45:27 -0600
committerGitHub <noreply@github.com>2025-01-10 16:45:27 -0600
commit0d16f01571ec8315f3979eae46981e559ade1cf9 (patch)
treeea43c32a57b0e6a67579d75a134dfbc009d09781
parent615d8f9d2ac56b3244d328587243301da253eafd (diff)
downloadazalea-drasl-0d16f01571ec8315f3979eae46981e559ade1cf9.tar.xz
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
-rwxr-xr-xREADME.md2
-rwxr-xr-xazalea-block/azalea-block-macros/src/lib.rs10
-rwxr-xr-xazalea-block/src/behavior.rs9
-rw-r--r--azalea-block/src/block_state.rs156
-rw-r--r--azalea-block/src/fluid_state.rs137
-rwxr-xr-xazalea-block/src/lib.rs241
-rw-r--r--azalea-block/src/range.rs6
-rw-r--r--azalea-client/src/mining.rs2
-rw-r--r--azalea-client/src/movement.rs7
-rw-r--r--azalea-client/src/packet_handling/game.rs7
-rwxr-xr-xazalea-core/src/aabb.rs376
-rwxr-xr-xazalea-core/src/direction.rs43
-rw-r--r--azalea-core/src/math.rs19
-rwxr-xr-xazalea-core/src/position.rs49
-rw-r--r--azalea-core/src/registry_holder.rs35
-rw-r--r--azalea-entity/src/attributes.rs1
-rwxr-xr-xazalea-entity/src/dimensions.rs11
-rw-r--r--azalea-entity/src/lib.rs89
-rw-r--r--azalea-entity/src/mining.rs4
-rw-r--r--azalea-entity/src/plugin/mod.rs10
-rw-r--r--azalea-physics/src/clip.rs154
-rwxr-xr-xazalea-physics/src/collision/discrete_voxel_shape.rs36
-rw-r--r--azalea-physics/src/collision/mod.rs44
-rwxr-xr-xazalea-physics/src/collision/shape.rs67
-rw-r--r--azalea-physics/src/collision/world_collisions.rs29
-rw-r--r--azalea-physics/src/fluids.rs274
-rw-r--r--azalea-physics/src/lib.rs731
-rw-r--r--azalea-physics/src/travel.rs299
-rw-r--r--azalea-physics/tests/physics.rs365
-rwxr-xr-xazalea-world/src/chunk_storage.rs3
-rw-r--r--azalea-world/src/container.rs8
-rw-r--r--azalea-world/src/find_blocks.rs2
-rwxr-xr-xazalea-world/src/palette.rs2
-rw-r--r--azalea-world/src/world.rs11
-rw-r--r--azalea/examples/testbot/commands/debug.rs15
-rw-r--r--azalea/src/auto_tool.rs10
-rw-r--r--azalea/src/pathfinder/mining.rs4
-rw-r--r--azalea/src/pathfinder/simulation.rs1
38 files changed, 2173 insertions, 1096 deletions
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<crate::BlockStateIntegerRepr> for #property_struct_name {
- fn from(value: crate::BlockStateIntegerRepr) -> Self {
+ impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
+ fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
match value {
#property_from_number_variants
_ => panic!("Invalid property value: {}", value),
@@ -360,8 +360,8 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct #property_struct_name(pub bool);
- impl From<crate::BlockStateIntegerRepr> for #property_struct_name {
- fn from(value: crate::BlockStateIntegerRepr) -> Self {
+ impl From<crate::block_state::BlockStateIntegerRepr> for #property_struct_name {
+ fn from(value: crate::block_state::BlockStateIntegerRepr) -> Self {
match value {
0 => Self(false),
1 => Self(true),
@@ -697,7 +697,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream {
let mut generated = quote! {
impl BlockState {
/// The highest possible block state ID.
- pub const MAX_STATE: crate::BlockStateIntegerRepr = #last_state_id;
+ pub const MAX_STATE: crate::block_state::BlockStateIntegerRepr = #last_state_id;
/// Get a property from this block state. Will be `None` if the block can't have the property.
///
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<bool>,
}
impl Default for BlockBehavior {
@@ -14,6 +16,7 @@ impl Default for BlockBehavior {
destroy_time: 0.,
explosion_resistance: 0.,
requires_correct_tool_for_drops: false,
+ force_solid: None,
}
}
}
@@ -52,4 +55,10 @@ impl BlockBehavior {
self.requires_correct_tool_for_drops = true;
self
}
+
+ // TODO: currently unused
+ pub fn force_solid(mut self, force_solid: bool) -> Self {
+ self.force_solid = Some(force_solid);
+ self
+ }
}
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<u32> for BlockState {
+ type Error = ();
+
+ /// Safely converts a u32 state id to a block state.
+ fn try_from(state_id: u32) -> Result<Self, Self::Error> {
+ let state_id = state_id as BlockStateIntegerRepr;
+ if Self::is_valid_state(state_id) {
+ Ok(BlockState { id: state_id })
+ } else {
+ Err(())
+ }
+ }
+}
+impl TryFrom<u16> for BlockState {
+ type Error = ();
+
+ /// Safely converts a u16 state id to a block state.
+ fn try_from(state_id: u16) -> Result<Self, Self::Error> {
+ let state_id = state_id as BlockStateIntegerRepr;
+ if Self::is_valid_state(state_id) {
+ Ok(BlockState { id: state_id })
+ } else {
+ Err(())
+ }
+ }
+}
+
+impl AzaleaRead for BlockState {
+ fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
+ let state_id = u32::azalea_read_var(buf)?;
+ Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
+ id: state_id as i32,
+ })
+ }
+}
+impl AzaleaWrite for BlockState {
+ fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
+ u32::azalea_write_var(&(self.id as u32), buf)
+ }
+}
+
+impl Debug for BlockState {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "BlockState(id: {}, {:?})",
+ self.id,
+ Box::<dyn Block>::from(*self)
+ )
+ }
+}
+
+impl From<BlockState> for azalea_registry::Block {
+ fn from(value: BlockState) -> Self {
+ Box::<dyn Block>::from(value).as_registry_block()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_from_u32() {
+ assert_eq!(
+ BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
+ BlockState::AIR
+ );
+
+ assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
+ assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
+ }
+
+ #[test]
+ fn test_from_blockstate() {
+ let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
+ assert_eq!(block.id(), "air");
+
+ let block: Box<dyn Block> =
+ Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
+ assert_eq!(block.id(), "flowering_azalea");
+ }
+
+ #[test]
+ fn test_debug_blockstate() {
+ let formatted = format!(
+ "{:?}",
+ BlockState::from(azalea_registry::Block::FloweringAzalea)
+ );
+ assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
+
+ let formatted = format!(
+ "{:?}",
+ BlockState::from(azalea_registry::Block::BigDripleafStem)
+ );
+ assert!(
+ formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
+ "{}",
+ formatted
+ );
+ }
+}
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<BlockState> for FluidState {
+ fn from(state: BlockState) -> Self {
+ // note that 8 here might be treated as 9 in some cases if there's another fluid
+ // block of the same type above it
+
+ if state
+ .property::<crate::properties::Waterlogged>()
+ .unwrap_or_default()
+ {
+ return Self {
+ kind: FluidKind::Water,
+ amount: 8,
+ falling: false,
+ };
+ }
+
+ let registry_block = azalea_registry::Block::from(state);
+ match registry_block {
+ azalea_registry::Block::Water => {
+ let level = state
+ .property::<crate::properties::WaterLevel>()
+ .expect("water block should always have WaterLevel");
+ return Self {
+ kind: FluidKind::Water,
+ amount: to_or_from_legacy_fluid_level(level as u8),
+ falling: false,
+ };
+ }
+ azalea_registry::Block::Lava => {
+ let level = state
+ .property::<crate::properties::LavaLevel>()
+ .expect("lava block should always have LavaLevel");
+ return Self {
+ kind: FluidKind::Lava,
+ amount: to_or_from_legacy_fluid_level(level as u8),
+ falling: false,
+ };
+ }
+ azalea_registry::Block::BubbleColumn => {
+ return Self::new_source_block(FluidKind::Water, false);
+ }
+ _ => {}
+ }
+
+ Self::default()
+ }
+}
+
+/// Sometimes Minecraft represents fluids with 0 being empty and 8 being full,
+/// and sometimes it's the opposite. You can use this function to convert
+/// in between those two representations.
+///
+/// You usually don't need to call this yourself, see [`FluidState`].
+pub fn to_or_from_legacy_fluid_level(level: u8) -> u8 {
+ // see FlowingFluid.getLegacyLevel
+ 8_u8.saturating_sub(level)
+}
+
+impl From<FluidState> for BlockState {
+ fn from(state: FluidState) -> Self {
+ match state.kind {
+ FluidKind::Empty => BlockState::AIR,
+ FluidKind::Water => BlockState::from(crate::blocks::Water {
+ level: crate::properties::WaterLevel::from(state.amount as BlockStateIntegerRepr),
+ }),
+ FluidKind::Lava => BlockState::from(crate::blocks::Lava {
+ level: crate::properties::LavaLevel::from(state.amount as BlockStateIntegerRepr),
+ }),
+ }
+ }
+}
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<Self::Value>;
}
-
-/// The type that's used internally to represent a block state ID.
-///
-/// This should be either `u16` or `u32`. If you choose to modify it, you must
-/// also change it in `azalea-block-macros/src/lib.rs`.
-///
-/// This does not affect protocol serialization, it just allows you to make the
-/// internal type smaller if you want.
-pub type BlockStateIntegerRepr = u16;
-
-/// A representation of a state a block can be in.
-///
-/// For example, a stone block only has one state but each possible stair
-/// rotation is a different state.
-///
-/// Note that this type is internally either a `u16` or `u32`, depending on
-/// [`BlockStateIntegerRepr`].
-#[derive(Copy, Clone, PartialEq, Eq, Default, Hash)]
-pub struct BlockState {
- /// The protocol ID for the block state. IDs may change every
- /// version, so you shouldn't hard-code them or store them in databases.
- pub id: BlockStateIntegerRepr,
-}
-
-impl BlockState {
- pub const AIR: BlockState = BlockState { id: 0 };
-
- #[inline]
- pub fn is_valid_state(state_id: BlockStateIntegerRepr) -> bool {
- state_id <= Self::MAX_STATE
- }
-
- /// Returns true if the block is air. This only checks for normal air, not
- /// other types like cave air.
- #[inline]
- pub fn is_air(&self) -> bool {
- self == &Self::AIR
- }
-}
-
-impl TryFrom<u32> for BlockState {
- type Error = ();
-
- /// Safely converts a u32 state id to a block state.
- fn try_from(state_id: u32) -> Result<Self, Self::Error> {
- let state_id = state_id as BlockStateIntegerRepr;
- if Self::is_valid_state(state_id) {
- Ok(BlockState { id: state_id })
- } else {
- Err(())
- }
- }
-}
-impl TryFrom<u16> for BlockState {
- type Error = ();
-
- /// Safely converts a u16 state id to a block state.
- fn try_from(state_id: u16) -> Result<Self, Self::Error> {
- let state_id = state_id as BlockStateIntegerRepr;
- if Self::is_valid_state(state_id) {
- Ok(BlockState { id: state_id })
- } else {
- Err(())
- }
- }
-}
-
-impl AzaleaRead for BlockState {
- fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
- let state_id = u32::azalea_read_var(buf)?;
- Self::try_from(state_id).map_err(|_| BufReadError::UnexpectedEnumVariant {
- id: state_id as i32,
- })
- }
-}
-impl AzaleaWrite for BlockState {
- fn azalea_write(&self, buf: &mut impl Write) -> Result<(), io::Error> {
- u32::azalea_write_var(&(self.id as u32), buf)
- }
-}
-
-impl Debug for BlockState {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(
- f,
- "BlockState(id: {}, {:?})",
- self.id,
- Box::<dyn Block>::from(*self)
- )
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct FluidState {
- pub fluid: azalea_registry::Fluid,
- /// 0 = empty, 8 = full, 9 = max.
- ///
- /// 9 is meant to be used when there's another fluid block of the same type
- /// above it, but it's usually unused by this struct.
- pub amount: u8,
-}
-impl FluidState {
- /// A floating point number in between 0 and 1 representing the height (as a
- /// percentage of a full block) of the fluid.
- pub fn height(&self) -> f32 {
- self.amount as f32 / 9.
- }
-}
-
-impl Default for FluidState {
- fn default() -> Self {
- Self {
- fluid: azalea_registry::Fluid::Empty,
- amount: 0,
- }
- }
-}
-
-impl From<BlockState> for FluidState {
- fn from(state: BlockState) -> Self {
- // note that 8 here might be treated as 9 in some cases if there's another fluid
- // block of the same type above it
-
- if state
- .property::<crate::properties::Waterlogged>()
- .unwrap_or_default()
- {
- Self {
- fluid: azalea_registry::Fluid::Water,
- amount: 8,
- }
- } else {
- let block = Box::<dyn Block>::from(state);
- if let Some(water) = block.downcast_ref::<crate::blocks::Water>() {
- Self {
- fluid: azalea_registry::Fluid::Water,
- amount: to_or_from_legacy_fluid_level(water.level as u8),
- }
- } else if let Some(lava) = block.downcast_ref::<crate::blocks::Lava>() {
- Self {
- fluid: azalea_registry::Fluid::Lava,
- amount: to_or_from_legacy_fluid_level(lava.level as u8),
- }
- } else {
- Self {
- fluid: azalea_registry::Fluid::Empty,
- amount: 0,
- }
- }
- }
- }
-}
-
-// see FlowingFluid.getLegacyLevel
-fn to_or_from_legacy_fluid_level(level: u8) -> u8 {
- 8_u8.saturating_sub(level)
-}
-
-impl From<FluidState> for BlockState {
- fn from(state: FluidState) -> Self {
- match state.fluid {
- azalea_registry::Fluid::Empty => BlockState::AIR,
- azalea_registry::Fluid::Water | azalea_registry::Fluid::FlowingWater => {
- BlockState::from(crate::blocks::Water {
- level: crate::properties::WaterLevel::from(
- state.amount as BlockStateIntegerRepr,
- ),
- })
- }
- azalea_registry::Fluid::Lava | azalea_registry::Fluid::FlowingLava => {
- BlockState::from(crate::blocks::Lava {
- level: crate::properties::LavaLevel::from(
- state.amount as BlockStateIntegerRepr,
- ),
- })
- }
- }
- }
-}
-
-impl From<BlockState> for azalea_registry::Block {
- fn from(value: BlockState) -> Self {
- Box::<dyn Block>::from(value).as_registry_block()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_from_u32() {
- assert_eq!(
- BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
- BlockState::AIR
- );
-
- assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
- assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
- }
-
- #[test]
- fn test_from_blockstate() {
- let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
- assert_eq!(block.id(), "air");
-
- let block: Box<dyn Block> =
- Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
- assert_eq!(block.id(), "flowering_azalea");
- }
-
- #[test]
- fn test_debug_blockstate() {
- let formatted = format!(
- "{:?}",
- BlockState::from(azalea_registry::Block::FloweringAzalea)
- );
- assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
-
- let formatted = format!(
- "{:?}",
- BlockState::from(azalea_registry::Block::BigDripleafStem)
- );
- assert!(
- formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
- "{}",
- formatted
- );
- }
-}
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<RangeInclusive<BlockStateIntegerRepr>> for BlockStates {
impl IntoIterator for BlockStates {
type Item = BlockState;
- type IntoIter = std::collections::hash_set::IntoIter<BlockState>;
+ type IntoIter = hash_set::IntoIter<BlockState>;
fn into_iter(self) -> Self::IntoIter {
self.set.into_iter()
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::<LookDirection>().unwrap();
if new_look_direction != *look_direction {
*look_direction = new_look_direction;
}
+ // old_pos is set to the current position when we're teleported
+ let mut physics = entity.get_mut::<Physics>().unwrap();
+ physics.set_old_pos(&position);
}),
});
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<Vec3> {
let mut t = 1.0;
let delta = max - min;
- let _dir = Self::get_direction(self, min, &mut t, None, &delta)?;
+ let _dir = Self::get_direction_aabb(self, min, &mut t, None, &delta)?;
Some(min + &(delta * t))
}
+ pub fn clip_with_from_and_to(min: &Vec3, max: &Vec3, from: &Vec3, to: &Vec3) -> Option<Vec3> {
+ let mut t = 1.0;
+ let delta = to - from;
+ let _dir = Self::get_direction(min, max, from, &mut t, None, &delta)?;
+ Some(from + &(delta * t))
+ }
+
pub fn clip_iterable(
boxes: &Vec<AABB>,
from: &Vec3,
@@ -245,8 +216,8 @@ impl AABB {
let delta = to - from;
for aabb in boxes {
- dir = Self::get_direction(
- &aabb.move_relative(&pos.to_vec3_floored()),
+ dir = Self::get_direction_aabb(
+ &aabb.move_relative(pos.to_vec3_floored()),
from,
&mut t,
dir,
@@ -264,8 +235,19 @@ impl AABB {
})
}
+ fn get_direction_aabb(
+ &self,
+ from: &Vec3,
+ t: &mut f64,
+ dir: Option<Direction>,
+ delta: &Vec3,
+ ) -> Option<Direction> {
+ AABB::get_direction(&self.min, &self.max, from, t, dir, delta)
+ }
+
fn get_direction(
- aabb: &AABB,
+ min: &Vec3,
+ max: &Vec3,
from: &Vec3,
t: &mut f64,
mut dir: Option<Direction>,
@@ -276,11 +258,11 @@ impl AABB {
t,
approach_dir: dir,
delta,
- begin: aabb.min_x,
- min_x: aabb.min_y,
- max_x: aabb.max_y,
- min_z: aabb.min_z,
- max_z: aabb.max_z,
+ begin: min.x,
+ min_x: min.y,
+ max_x: max.y,
+ min_z: min.z,
+ max_z: max.z,
result_dir: Direction::West,
start: from,
});
@@ -289,11 +271,11 @@ impl AABB {
t,
approach_dir: dir,
delta,
- begin: aabb.max_x,
- min_x: aabb.min_y,
- max_x: aabb.max_y,
- min_z: aabb.min_z,
- max_z: aabb.max_z,
+ begin: max.x,
+ min_x: min.y,
+ max_x: max.y,
+ min_z: min.z,
+ max_z: max.z,
result_dir: Direction::East,
start: from,
});
@@ -308,11 +290,11 @@ impl AABB {
y: delta.z,
z: delta.x,
},
- begin: aabb.min_y,
- min_x: aabb.min_z,
- max_x: aabb.max_z,
- min_z: aabb.min_x,
- max_z: aabb.max_x,
+ begin: min.y,
+ min_x: min.z,
+ max_x: max.z,
+ min_z: min.x,
+ max_z: max.x,
result_dir: Direction::Down,
start: &Vec3 {
x: from.y,
@@ -329,11 +311,11 @@ impl AABB {
y: delta.z,
z: delta.x,
},
- begin: aabb.max_y,
- min_x: aabb.min_z,
- max_x: aabb.max_z,
- min_z: aabb.min_x,
- max_z: aabb.max_x,
+ begin: max.y,
+ min_x: min.z,
+ max_x: max.z,
+ min_z: min.x,
+ max_z: max.x,
result_dir: Direction::Up,
start: &Vec3 {
x: from.y,
@@ -352,11 +334,11 @@ impl AABB {
y: delta.x,
z: delta.y,
},
- begin: aabb.min_z,
- min_x: aabb.min_x,
- max_x: aabb.max_x,
- min_z: aabb.min_y,
- max_z: aabb.max_y,
+ begin: min.z,
+ min_x: min.x,
+ max_x: max.x,
+ min_z: min.y,
+ max_z: max.y,
result_dir: Direction::North,
start: &Vec3 {
x: from.z,
@@ -373,11 +355,11 @@ impl AABB {
y: delta.x,
z: delta.y,
},
- begin: aabb.max_z,
- min_x: aabb.min_x,
- max_x: aabb.max_x,
- min_z: aabb.min_y,
- max_z: aabb.max_y,
+ begin: max.z,
+ min_x: min.x,
+ max_x: max.x,
+ min_z: min.y,
+ max_z: max.y,
result_dir: Direction::South,
start: &Vec3 {
x: from.z,
@@ -409,38 +391,96 @@ impl AABB {
}
pub fn has_nan(&self) -> bool {
- self.min_x.is_nan()
- || self.min_y.is_nan()
- || self.min_z.is_nan()
- || self.max_x.is_nan()
- || self.max_y.is_nan()
- || self.max_z.is_nan()
+ self.min.x.is_nan()
+ || self.min.y.is_nan()
+ || self.min.z.is_nan()
+ || self.max.x.is_nan()
+ || self.max.y.is_nan()
+ || self.max.z.is_nan()
}
pub fn get_center(&self) -> Vec3 {
Vec3::new(
- (self.min_x + self.max_x) / 2.0,
- (self.min_y + self.max_y) / 2.0,
- (self.min_z + self.max_z) / 2.0,
+ (self.min.x + self.max.x) / 2.0,
+ (self.min.y + self.max.y) / 2.0,
+ (self.min.z + self.max.z) / 2.0,
)
}
pub fn of_size(center: Vec3, dx: f64, dy: f64, dz: f64) -> AABB {
AABB {
- min_x: center.x - dx / 2.0,
- min_y: center.y - dy / 2.0,
- min_z: center.z - dz / 2.0,
- max_x: center.x + dx / 2.0,
- max_y: center.y + dy / 2.0,
- max_z: center.z + dz / 2.0,
+ min: Vec3::new(
+ center.x - dx / 2.0,
+ center.y - dy / 2.0,
+ center.z - dz / 2.0,
+ ),
+ max: Vec3::new(
+ center.x + dx / 2.0,
+ center.y + dy / 2.0,
+ center.z + dz / 2.0,
+ ),
}
}
pub fn max(&self, axis: &Axis) -> f64 {
- axis.choose(self.max_x, self.max_y, self.max_z)
+ axis.choose(self.max.x, self.max.y, self.max.z)
}
pub fn min(&self, axis: &Axis) -> f64 {
- axis.choose(self.min_x, self.min_y, self.min_z)
+ axis.choose(self.min.x, self.min.y, self.min.z)
+ }
+
+ pub fn collided_along_vector(&self, vector: Vec3, boxes: &Vec<AABB>) -> bool {
+ let center = self.get_center();
+ let new_center = center + vector;
+
+ for aabb in boxes {
+ let inflated = aabb.inflate(
+ self.get_size(Axis::X) * 0.5,
+ self.get_size(Axis::Y) * 0.5,
+ self.get_size(Axis::Z) * 0.5,
+ );
+ if inflated.contains(&new_center) || inflated.contains(&center) {
+ return true;
+ }
+
+ if inflated.clip(&center, &new_center).is_some() {
+ return true;
+ }
+ }
+
+ false
+ }
+}
+
+impl BlockPos {
+ pub fn between_closed_aabb(aabb: &AABB) -> Vec<BlockPos> {
+ BlockPos::between_closed(BlockPos::from(aabb.min), BlockPos::from(aabb.max))
+ }
+
+ pub fn between_closed(min: BlockPos, max: BlockPos) -> Vec<BlockPos> {
+ assert!(min.x <= max.x);
+ assert!(min.y <= max.y);
+ assert!(min.z <= max.z);
+
+ let length_x = max.x - min.x + 1;
+ let length_y = max.y - min.y + 1;
+ let length_z = max.z - min.z + 1;
+ let volume = length_x * length_y * length_z;
+
+ let mut result = Vec::with_capacity(volume as usize);
+ for index in 0..volume {
+ let index_x = index % length_x;
+ let remaining_after_x = index / length_x;
+ let index_y = remaining_after_x % length_y;
+ let index_z = remaining_after_x / length_y;
+ result.push(BlockPos::new(
+ min.x + index_x,
+ min.y + index_y,
+ min.z + index_z,
+ ));
+ }
+
+ result
}
}
@@ -453,12 +493,8 @@ mod tests {
assert_ne!(
AABB::clip_iterable(
&vec![AABB {
- min_x: 0.,
- min_y: 0.,
- min_z: 0.,
- max_x: 1.,
- max_y: 1.,
- max_z: 1.,
+ min: Vec3::new(0., 0., 0.),
+ max: Vec3::new(1., 1., 1.),
}],
&Vec3::new(-1., -1., -1.),
&Vec3::new(1., 1., 1.),
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<RegistryType<DimensionTypeElement>> {
+ let name = ResourceLocation::new("minecraft:dimension_type");
+ match self.get(&name) {
+ Some(Ok(registry)) => Some(registry),
+ Some(Err(err)) => {
+ error!(
+ "Error deserializing dimension type registry: {err:?}\n{:?}",
+ self.map.get(&name)
+ );
+ None
+ }
+ None => None,
+ }
+ }
+
fn get<T: Deserialize>(
&self,
name: &ResourceLocation,
@@ -66,23 +83,6 @@ impl RegistryHolder {
Some(Ok(RegistryType { map }))
}
-
- /// Get the dimension type registry, or `None` if it doesn't exist. You
- /// should do some type of error handling if this returns `None`.
- pub fn dimension_type(&self) -> Option<RegistryType<DimensionTypeElement>> {
- let name = ResourceLocation::new("minecraft:dimension_type");
- match self.get(&name) {
- Some(Ok(registry)) => Some(registry),
- Some(Err(err)) => {
- error!(
- "Error deserializing dimension type registry: {err:?}\n{:?}",
- self.map.get(&name)
- );
- None
- }
- None => None,
- }
- }
}
/// A collection of values for a certain type of registry data.
@@ -161,6 +161,7 @@ pub struct DimensionTypeElement {
pub struct DimensionTypeElement {
pub height: u32,
pub min_y: i32,
+ pub ultrawarm: bool,
#[simdnbt(flatten)]
pub _extra: HashMap<String, NbtTag>,
}
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,
+
+ /// The position of the entity before it moved this tick.
+ ///
+ /// This is set immediately before physics is done.
+ pub old_position: Vec3,
- /// X acceleration.
- pub xxa: f32,
- /// Y acceleration.
- pub yya: f32,
- /// Z acceleration.
- pub zza: f32,
+ /// 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),
+
+ old_position: pos,
- xxa: 0.,
- yya: 0.,
- zza: 0.,
+ 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<Position>>) {
for (position, mut physics) in query.iter_mut() {
- let bounding_box = physics.dimensions.make_bounding_box(**position);
+ let bounding_box = physics.dimensions.make_bounding_box(position);
physics.bounding_box = bounding_box;
}
}
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<C, T>(
let vec = right_after_end - right_before_start;
- /// Returns either -1, 0, or 1, depending on whether the number is negative,
- /// zero, or positive.
- ///
- /// This function exists because f64::signum doesn't check for 0.
- fn get_number_sign(num: f64) -> f64 {
- if num == 0. {
- 0.
- } else {
- num.signum()
- }
- }
-
let vec_sign = Vec3 {
- x: get_number_sign(vec.x),
- y: get_number_sign(vec.y),
- z: get_number_sign(vec.z),
+ x: math::sign(vec.x),
+ y: math::sign(vec.y),
+ z: math::sign(vec.z),
};
#[rustfmt::skip]
@@ -270,3 +264,125 @@ pub fn traverse_blocks<C, T>(
}
}
}
+
+pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet<BlockPos> {
+ let delta = to - from;
+ let traversed_blocks = BlockPos::between_closed_aabb(aabb);
+ if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 {
+ return traversed_blocks.into_iter().collect();
+ }
+
+ let mut traversed_and_collided_blocks = HashSet::new();
+ let target_min_pos = aabb.min;
+ let from_min_pos = target_min_pos - delta;
+ add_collisions_along_travel(
+ &mut traversed_and_collided_blocks,
+ from_min_pos,
+ target_min_pos,
+ *aabb,
+ );
+ traversed_and_collided_blocks.extend(traversed_blocks);
+ traversed_and_collided_blocks
+}
+
+pub fn add_collisions_along_travel(
+ collisions: &mut HashSet<BlockPos>,
+ from: Vec3,
+ to: Vec3,
+ aabb: AABB,
+) {
+ let delta = to - from;
+ let mut min_x = from.x.floor() as i32;
+ let mut min_y = from.y.floor() as i32;
+ let mut min_z = from.z.floor() as i32;
+ let direction_x = math::sign_as_int(delta.x);
+ let direction_y = math::sign_as_int(delta.y);
+ let direction_z = math::sign_as_int(delta.z);
+ let step_x = if direction_x == 0 {
+ f64::MAX
+ } else {
+ direction_x as f64 / delta.x
+ };
+ let step_y = if direction_y == 0 {
+ f64::MAX
+ } else {
+ direction_y as f64 / delta.y
+ };
+ let step_z = if direction_z == 0 {
+ f64::MAX
+ } else {
+ direction_z as f64 / delta.z
+ };
+ let mut cur_x = step_x
+ * if direction_x > 0 {
+ 1. - math::fract(from.x)
+ } else {
+ math::fract(from.x)
+ };
+ let mut cur_y = step_y
+ * if direction_y > 0 {
+ 1. - math::fract(from.y)
+ } else {
+ math::fract(from.y)
+ };
+ let mut cur_z = step_z
+ * if direction_z > 0 {
+ 1. - math::fract(from.z)
+ } else {
+ math::fract(from.z)
+ };
+ let mut step_count = 0;
+
+ while cur_x <= 1. || cur_y <= 1. || cur_z <= 1. {
+ if cur_x < cur_y {
+ if cur_x < cur_z {
+ min_x += direction_x;
+ cur_x += step_x;
+ } else {
+ min_z += direction_z;
+ cur_z += step_z;
+ }
+ } else if cur_y < cur_z {
+ min_y += direction_y;
+ cur_y += step_y;
+ } else {
+ min_z += direction_z;
+ cur_z += step_z;
+ }
+
+ if step_count > 16 {
+ break;
+ }
+ step_count += 1;
+
+ let Some(clip_location) = AABB::clip_with_from_and_to(
+ &Vec3::new(min_x as f64, min_y as f64, min_z as f64),
+ &Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64),
+ &from,
+ &to,
+ ) else {
+ continue;
+ };
+
+ let initial_max_x = clip_location
+ .x
+ .clamp(min_x as f64 + 1.0E-5, min_x as f64 + 1.0 - 1.0E-5);
+ let initial_max_y = clip_location
+ .y
+ .clamp(min_y as f64 + 1.0E-5, min_y as f64 + 1.0 - 1.0E-5);
+ let initial_max_z = clip_location
+ .z
+ .clamp(min_z as f64 + 1.0E-5, min_z as f64 + 1.0 - 1.0E-5);
+ let max_x = (initial_max_x + aabb.get_size(Axis::X)).floor() as i32;
+ let max_y = (initial_max_y + aabb.get_size(Axis::Y)).floor() as i32;
+ let max_z = (initial_max_z + aabb.get_size(Axis::Z)).floor() as i32;
+
+ for x in min_x..=max_x {
+ for y in min_y..=max_y {
+ for z in min_z..=max_z {
+ collisions.insert(BlockPos::new(x, y, z));
+ }
+ }
+ }
+ }
+}
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<azalea_entity::Position>,
@@ -296,7 +299,7 @@ fn collide_with_shapes(
if y_movement != 0. {
y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement);
if y_movement != 0. {
- entity_box = entity_box.move_relative(&Vec3 {
+ entity_box = entity_box.move_relative(Vec3 {
x: 0.,
y: y_movement,
z: 0.,
@@ -311,7 +314,7 @@ fn collide_with_shapes(
if more_z_movement && z_movement != 0. {
z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement);
if z_movement != 0. {
- entity_box = entity_box.move_relative(&Vec3 {
+ entity_box = entity_box.move_relative(Vec3 {
x: 0.,
y: 0.,
z: z_movement,
@@ -322,7 +325,7 @@ fn collide_with_shapes(
if x_movement != 0. {
x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement);
if x_movement != 0. {
- entity_box = entity_box.move_relative(&Vec3 {
+ entity_box = entity_box.move_relative(Vec3 {
x: x_movement,
y: 0.,
z: 0.,
@@ -352,7 +355,7 @@ pub fn fluid_shape(
) -> &'static VoxelShape {
if fluid.amount == 9 {
let fluid_state_above = world.get_fluid_state(&pos.up(1)).unwrap_or_default();
- if fluid_state_above.fluid == fluid.fluid {
+ if fluid_state_above.kind == fluid.kind {
return &BLOCK_SHAPE;
}
}
@@ -384,3 +387,28 @@ pub fn fluid_shape(
fn calculate_shape_for_fluid(amount: u8) -> VoxelShape {
box_shape(0.0, 0.0, 0.0, 1.0, (f32::from(amount) / 9.0) as f64, 1.0)
}
+
+/// Whether the block is treated as "motion blocking".
+///
+/// This is marked as deprecated in Minecraft.
+pub fn legacy_blocks_motion(block: BlockState) -> bool {
+ let registry_block = azalea_registry::Block::from(block);
+ legacy_calculate_solid(block)
+ && registry_block != azalea_registry::Block::Cobweb
+ && registry_block != azalea_registry::Block::BambooSapling
+}
+
+pub fn legacy_calculate_solid(block: BlockState) -> bool {
+ // force_solid has to be checked before anything else
+ let block_trait = Box::<dyn azalea_block::Block>::from(block);
+ if let Some(solid) = block_trait.behavior().force_solid {
+ return solid;
+ }
+
+ let shape = block.collision_shape();
+ if shape.is_empty() {
+ return false;
+ }
+ let bounds = shape.bounds();
+ bounds.size() >= 0.7291666666666666 || bounds.get_size(Axis::Y) >= 1.0
+}
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<AABB> for VoxelShape {
fn from(aabb: AABB) -> Self {
box_shape_unchecked(
- aabb.min_x, aabb.min_y, aabb.min_z, aabb.max_x, aabb.max_y, aabb.max_z,
+ aabb.min.x, aabb.min.y, aabb.min.z, aabb.max.x, aabb.max.y, aabb.max.z,
)
}
}
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<VoxelShape> {
// if it's a full block do a faster collision check
if block_state.is_collision_shape_full() {
if !state.aabb.intersects_aabb(&AABB {
- min_x: item.pos.x as f64,
- min_y: item.pos.y as f64,
- min_z: item.pos.z as f64,
- max_x: (item.pos.x + 1) as f64,
- max_y: (item.pos.y + 1) as f64,
- max_z: (item.pos.z + 1) as f64,
+ min: item.pos.to_vec3_floored(),
+ max: (item.pos + 1).to_vec3_floored(),
}) {
continue;
}
- block_collisions.push(BLOCK_SHAPE.move_relative(
- item.pos.x as f64,
- item.pos.y as f64,
- item.pos.z as f64,
- ));
+ block_collisions.push(BLOCK_SHAPE.move_relative(item.pos.to_vec3_floored()));
continue;
}
let block_shape = state.get_block_shape(block_state);
- let block_shape =
- block_shape.move_relative(item.pos.x as f64, item.pos.y as f64, item.pos.z as f64);
+ let block_shape = block_shape.move_relative(item.pos.to_vec3_floored());
// if the entity shape and block shape don't collide, continue
if !Shapes::matches_anywhere(&block_shape, &state.entity_shape, |a, b| a && b) {
continue;
@@ -95,15 +86,15 @@ pub struct BlockCollisionsState<'a> {
impl<'a> BlockCollisionsState<'a> {
pub fn new(world: &'a Instance, aabb: AABB) -> Self {
let origin = BlockPos {
- x: (aabb.min_x - EPSILON).floor() as i32 - 1,
- y: (aabb.min_y - EPSILON).floor() as i32 - 1,
- z: (aabb.min_z - EPSILON).floor() as i32 - 1,
+ x: (aabb.min.x - EPSILON).floor() as i32 - 1,
+ y: (aabb.min.y - EPSILON).floor() as i32 - 1,
+ z: (aabb.min.z - EPSILON).floor() as i32 - 1,
};
let end = BlockPos {
- x: (aabb.max_x + EPSILON).floor() as i32 + 1,
- y: (aabb.max_y + EPSILON).floor() as i32 + 1,
- z: (aabb.max_z + EPSILON).floor() as i32 + 1,
+ x: (aabb.max.x + EPSILON).floor() as i32 + 1,
+ y: (aabb.max.y + EPSILON).floor() as i32 + 1,
+ z: (aabb.max.z + EPSILON).floor() as i32 + 1,
};
let cursor = Cursor3d::new(origin, end);
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<LocalEntity>, With<InLoadedChunk>),
+ >,
+ instance_container: Res<InstanceContainer>,
+) {
+ for (mut physics, position, instance_name) in &mut query {
+ let world_lock = instance_container
+ .get(instance_name)
+ .expect("All entities should be in a valid world");
+ let world = world_lock.read();
+
+ physics.water_fluid_height = 0.;
+ physics.lava_fluid_height = 0.;
+
+ update_in_water_state_and_do_water_current_pushing(&mut physics, &world, position);
+
+ let is_ultrawarm = world
+ .registries
+ .dimension_type()
+ .and_then(|d| d.map.get(instance_name).map(|d| d.ultrawarm))
+ == Some(true);
+ let lava_push_factor = if is_ultrawarm {
+ 0.007
+ } else {
+ 0.0023333333333333335
+ };
+
+ update_fluid_height_and_do_fluid_pushing(
+ &mut physics,
+ &world,
+ FluidKind::Lava,
+ lava_push_factor,
+ );
+ }
+}
+fn update_in_water_state_and_do_water_current_pushing(
+ physics: &mut Physics,
+ world: &Instance,
+ _position: &Position,
+) {
+ // TODO: implement vehicles and boats
+ // if vehicle == AbstractBoat {
+ // if !boat.is_underwater() {
+ // *was_touching_water = false;
+ // }
+ // }
+
+ // updateFluidHeightAndDoFluidPushing
+ if update_fluid_height_and_do_fluid_pushing(physics, world, FluidKind::Water, 0.014) {
+ // if !was_touching_water && !first_tick {
+ // do_water_splash_effect();
+ // }
+
+ physics.reset_fall_distance();
+ physics.was_touching_water = true;
+ physics.clear_fire();
+ } else {
+ physics.was_touching_water = false;
+ }
+}
+
+fn update_fluid_height_and_do_fluid_pushing(
+ physics: &mut Physics,
+ world: &Instance,
+ checking_fluid: FluidKind,
+ fluid_push_factor: f64,
+) -> bool {
+ // if touching_unloaded_chunk() {
+ // return false;
+ // }
+
+ let checking_liquids_aabb = physics.bounding_box.deflate_all(0.001);
+
+ let min_x = checking_liquids_aabb.min.x.floor() as i32;
+ let min_y = checking_liquids_aabb.min.y.floor() as i32;
+ let min_z = checking_liquids_aabb.min.z.floor() as i32;
+
+ let max_x = checking_liquids_aabb.max.x.ceil() as i32;
+ let max_y = checking_liquids_aabb.max.y.ceil() as i32;
+ let max_z = checking_liquids_aabb.max.z.ceil() as i32;
+
+ let mut min_height_touching = 0.;
+ let is_entity_pushable_by_fluid = true;
+ let mut touching_fluid = false;
+ let mut additional_player_delta = Vec3::default();
+ let mut num_fluids_being_touched = 0;
+
+ for cur_x in min_x..=max_x {
+ for cur_y in min_y..=max_y {
+ for cur_z in min_z..=max_z {
+ let cur_pos = BlockPos::new(cur_x, cur_y, cur_z);
+ let Some(fluid_at_cur_pos) = world.get_fluid_state(&cur_pos) else {
+ continue;
+ };
+ if fluid_at_cur_pos.kind != checking_fluid {
+ continue;
+ }
+ let fluid_max_y = (cur_y as f32 + fluid_at_cur_pos.height()) as f64;
+ if fluid_max_y < checking_liquids_aabb.min.y {
+ continue;
+ }
+ touching_fluid = true;
+ min_height_touching = f64::max(
+ fluid_max_y - checking_liquids_aabb.min.y,
+ min_height_touching,
+ );
+ if !is_entity_pushable_by_fluid {
+ continue;
+ }
+ let mut additional_player_delta_for_fluid =
+ get_fluid_flow(&fluid_at_cur_pos, world, cur_pos);
+ if min_height_touching < 0.4 {
+ additional_player_delta_for_fluid *= min_height_touching;
+ };
+
+ additional_player_delta += additional_player_delta_for_fluid;
+ num_fluids_being_touched += 1;
+ }
+ }
+ }
+
+ if additional_player_delta.length() > 0. {
+ additional_player_delta /= num_fluids_being_touched as f64;
+
+ // if entity_kind != EntityKind::Player {
+ // additional_player_delta = additional_player_delta.normalize();
+ // }
+
+ let player_delta = physics.velocity;
+ additional_player_delta *= fluid_push_factor;
+ const MIN_PUSH: f64 = 0.003;
+ const MIN_PUSH_LENGTH: f64 = MIN_PUSH * 1.5;
+
+ if player_delta.x.abs() < MIN_PUSH
+ && player_delta.z.abs() < MIN_PUSH
+ && additional_player_delta.length() < MIN_PUSH_LENGTH
+ {
+ additional_player_delta = additional_player_delta.normalize() * MIN_PUSH_LENGTH;
+ }
+
+ physics.velocity += additional_player_delta;
+ }
+
+ match checking_fluid {
+ FluidKind::Water => physics.water_fluid_height = min_height_touching,
+ FluidKind::Lava => physics.lava_fluid_height = min_height_touching,
+ FluidKind::Empty => panic!("FluidKind::Empty should not be passed to update_fluid_height"),
+ };
+
+ touching_fluid
+}
+
+pub fn update_swimming() {
+ // TODO: swimming
+}
+
+// FlowingFluid.getFlow
+pub fn get_fluid_flow(fluid: &FluidState, world: &Instance, pos: BlockPos) -> Vec3 {
+ let mut z_flow: f64 = 0.;
+ let mut x_flow: f64 = 0.;
+
+ for direction in Direction::HORIZONTAL {
+ let adjacent_block_pos = pos.offset_with_direction(direction);
+ let adjacent_fluid_state = world
+ .get_fluid_state(&adjacent_block_pos)
+ .unwrap_or_default();
+ if fluid.affects_flow(&adjacent_fluid_state) {
+ let mut adjacent_fluid_height = adjacent_fluid_state.height();
+ let mut adjacent_height_difference: f32 = 0.;
+
+ if adjacent_fluid_height == 0. {
+ if !legacy_blocks_motion(
+ world
+ .get_block_state(&adjacent_block_pos)
+ .unwrap_or_default(),
+ ) {
+ let block_pos_below_adjacent = adjacent_block_pos.down(1);
+ let fluid_below_adjacent = world
+ .get_fluid_state(&block_pos_below_adjacent)
+ .unwrap_or_default();
+
+ if fluid.affects_flow(&fluid_below_adjacent) {
+ adjacent_fluid_height = fluid_below_adjacent.height();
+ if adjacent_fluid_height > 0. {
+ adjacent_height_difference =
+ fluid.height() - (adjacent_fluid_height - 0.8888889);
+ }
+ }
+ }
+ } else if adjacent_fluid_height > 0. {
+ adjacent_height_difference = fluid.height() - adjacent_fluid_height;
+ }
+
+ if adjacent_height_difference != 0. {
+ x_flow += (direction.x() as f32 * adjacent_height_difference) as f64;
+ z_flow += (direction.z() as f32 * adjacent_height_difference) as f64;
+ }
+ }
+ }
+
+ let mut flow = Vec3::new(x_flow, 0., z_flow);
+ if fluid.falling {
+ for direction in Direction::HORIZONTAL {
+ let adjacent_block_pos = pos.offset_with_direction(direction);
+ if is_solid_face(fluid, world, adjacent_block_pos, direction)
+ || is_solid_face(fluid, world, adjacent_block_pos.up(1), direction)
+ {
+ flow = flow.normalize() + Vec3::new(0., -6., 0.);
+ break;
+ }
+ }
+ }
+
+ flow.normalize()
+}
+
+// i don't really get what this is for
+fn is_solid_face(
+ fluid: &FluidState,
+ world: &Instance,
+ adjacent_pos: BlockPos,
+ direction: Direction,
+) -> bool {
+ let block_state = world.get_block_state(&adjacent_pos).unwrap_or_default();
+ let fluid_state = world.get_fluid_state(&adjacent_pos).unwrap_or_default();
+ if fluid_state.is_same_kind(fluid) {
+ return false;
+ }
+ if direction == Direction::Up {
+ return true;
+ }
+ let registry_block = azalea_registry::Block::from(block_state);
+ if matches!(
+ registry_block,
+ // frosted ice is from frost walker
+ azalea_registry::Block::Ice | azalea_registry::Block::FrostedIce
+ ) {
+ return false;
+ }
+ is_face_sturdy(block_state, world, adjacent_pos, direction)
+}
+
+fn is_face_sturdy(
+ _block_state: BlockState,
+ _world: &Instance,
+ _pos: BlockPos,
+ _direction: Direction,
+) -> bool {
+ // TODO: this does a whole bunch of physics shape checks for waterlogged blocks
+ // that i honestly cannot be bothered to implement right now
+
+ // see BlockBehavior.isFaceSturdy in the decompiled minecraft source
+
+ // also, this probably should be in a module other than fluids.rs
+
+ false
+}
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<LocalEntity>, With<InLoadedChunk>),
- >,
- instance_container: Res<InstanceContainer>,
-) {
- for (
- mut physics,
- direction,
- position,
- sprinting,
- pose,
- attributes,
- world_name,
- on_climbable,
- jumping,
- ) in &mut query
- {
- let Some(world_lock) = instance_container.get(world_name) else {
- continue;
- };
- let world = world_lock.read();
- // if !self.is_effective_ai() && !self.is_controlled_by_local_instance() {
- // // this.calculateEntityAnimation(this, this instanceof FlyingAnimal);
- // return;
- // }
-
- let gravity: f64 = 0.08;
-
- // TODO: slow falling effect
- // let is_falling = self.delta.y <= 0.;
-
- // TODO: fluids
-
- // TODO: elytra
-
- let block_pos_below = get_block_pos_below_that_affects_movement(&position);
-
- let block_state_below = world
- .chunks
- .get_block_state(&block_pos_below)
- .unwrap_or(BlockState::AIR);
- let block_below: Box<dyn Block> = block_state_below.into();
- let block_friction = block_below.behavior().friction;
-
- let inertia = if physics.on_ground() {
- block_friction * 0.91
- } else {
- 0.91
- };
-
- // this applies the current delta
- let mut movement = handle_relative_friction_and_calculate_movement(
- HandleRelativeFrictionAndCalculateMovementOpts {
- block_friction,
- world: &world,
- physics: &mut physics,
- direction: &direction,
- position,
- attributes,
- is_sprinting: sprinting.map(|s| **s).unwrap_or(false),
- on_climbable,
- pose,
- jumping,
- },
- );
-
- movement.y -= gravity;
-
- // if (this.shouldDiscardFriction()) {
- // this.setDeltaMovement(movement.x, yMovement, movement.z);
- // } else {
- // this.setDeltaMovement(movement.x * (double)inertia, yMovement *
- // 0.9800000190734863D, movement.z * (double)inertia); }
-
- // if should_discard_friction(self) {
- if false {
- physics.velocity = movement;
- } else {
- physics.velocity = Vec3 {
- x: movement.x * inertia as f64,
- y: movement.y * 0.9800000190734863f64,
- z: movement.z * inertia as f64,
- };
- }
- }
-}
-
-/// applies air resistance, calls self.travel(), and some other random
-/// stuff.
+/// Applies air resistance and handles jumping.
+///
+/// Happens before [`travel::travel`].
#[allow(clippy::type_complexity)]
pub fn ai_step(
mut query: Query<
@@ -164,6 +76,10 @@ pub fn ai_step(
// vanilla does movement interpolation here, doesn't really matter much for a
// bot though
+ if physics.no_jump_delay > 0 {
+ physics.no_jump_delay -= 1;
+ }
+
if physics.velocity.x.abs() < 0.003 {
physics.velocity.x = 0.;
}
@@ -178,27 +94,221 @@ pub fn ai_step(
if **jumping {
// TODO: jumping in liquids and jump delay
- if physics.on_ground() {
- jump_from_ground(
- &mut physics,
- position,
- look_direction,
- sprinting,
- instance_name,
- &instance_container,
- )
+ let fluid_height = if physics.is_in_lava() {
+ physics.lava_fluid_height
+ } else if physics.is_in_water() {
+ physics.water_fluid_height
+ } else {
+ 0.
+ };
+
+ let in_water = physics.is_in_water() && fluid_height > 0.;
+ let fluid_jump_threshold = travel::fluid_jump_threshold();
+
+ if !in_water || physics.on_ground() && fluid_height <= fluid_jump_threshold {
+ if !physics.is_in_lava()
+ || physics.on_ground() && fluid_height <= fluid_jump_threshold
+ {
+ if physics.on_ground()
+ || in_water
+ && fluid_height <= fluid_jump_threshold
+ && physics.no_jump_delay == 0
+ {
+ jump_from_ground(
+ &mut physics,
+ position,
+ look_direction,
+ sprinting,
+ instance_name,
+ &instance_container,
+ );
+ physics.no_jump_delay = 10;
+ }
+ } else {
+ jump_in_liquid(&mut physics);
+ }
+ } else {
+ jump_in_liquid(&mut physics);
}
}
+ } else {
+ physics.no_jump_delay = 0;
}
- physics.xxa *= 0.98;
- physics.zza *= 0.98;
+ physics.x_acceleration *= 0.98;
+ physics.z_acceleration *= 0.98;
// TODO: freezing, pushEntities, drowning damage (in their own systems,
// after `travel`)
}
}
+fn jump_in_liquid(physics: &mut Physics) {
+ physics.velocity.y += 0.04;
+}
+
+// in minecraft, this is done as part of aiStep immediately after travel
+#[allow(clippy::type_complexity)]
+pub fn apply_effects_from_blocks(
+ mut query: Query<
+ (&mut Physics, &Position, &InstanceName),
+ (With<LocalEntity>, With<InLoadedChunk>),
+ >,
+ instance_container: Res<InstanceContainer>,
+) {
+ for (mut physics, position, world_name) in &mut query {
+ let Some(world_lock) = instance_container.get(world_name) else {
+ continue;
+ };
+ let world = world_lock.read();
+
+ // if !is_affected_by_blocks {
+ // continue
+ // }
+
+ // if (this.onGround()) {
+ // BlockPos var3 = this.getOnPosLegacy();
+ // BlockState var4 = this.level().getBlockState(var3);
+ // var4.getBlock().stepOn(this.level(), var3, var4, this);
+ // }
+
+ // minecraft adds more entries to the list when the code is running on the
+ // server
+ let movement_this_tick = [EntityMovement {
+ from: physics.old_position,
+ to: **position,
+ }];
+
+ check_inside_blocks(&mut physics, &world, &movement_this_tick);
+ }
+}
+
+fn check_inside_blocks(
+ physics: &mut Physics,
+ world: &Instance,
+ movements: &[EntityMovement],
+) -> Vec<BlockState> {
+ let mut blocks_inside = Vec::new();
+ let mut visited_blocks = HashSet::<BlockState>::new();
+
+ for movement in movements {
+ let bounding_box_at_target = physics
+ .dimensions
+ .make_bounding_box(&movement.to)
+ .deflate_all(1.0E-5);
+
+ for traversed_block in
+ box_traverse_blocks(&movement.from, &movement.to, &bounding_box_at_target)
+ {
+ // if (!this.isAlive()) {
+ // return;
+ // }
+
+ let traversed_block_state = world.get_block_state(&traversed_block).unwrap_or_default();
+ if traversed_block_state.is_air() {
+ continue;
+ }
+ if !visited_blocks.insert(traversed_block_state) {
+ continue;
+ }
+
+ /*
+ VoxelShape var12 = traversedBlockState.getEntityInsideCollisionShape(this.level(), traversedBlock);
+ if (var12 != Shapes.block() && !this.collidedWithShapeMovingFrom(from, to, traversedBlock, var12)) {
+ continue;
+ }
+
+ traversedBlockState.entityInside(this.level(), traversedBlock, this);
+ this.onInsideBlock(traversedBlockState);
+ */
+
+ // this is different for end portal frames and tripwire hooks, i don't think it
+ // actually matters for a client though
+ let entity_inside_collision_shape = &*BLOCK_SHAPE;
+
+ if entity_inside_collision_shape != &*BLOCK_SHAPE
+ && !collided_with_shape_moving_from(
+ &movement.from,
+ &movement.to,
+ traversed_block,
+ entity_inside_collision_shape,
+ physics,
+ )
+ {
+ continue;
+ }
+
+ handle_entity_inside_block(world, traversed_block_state, traversed_block, physics);
+
+ blocks_inside.push(traversed_block_state);
+ }
+ }
+
+ blocks_inside
+}
+
+fn collided_with_shape_moving_from(
+ from: &Vec3,
+ to: &Vec3,
+ traversed_block: BlockPos,
+ entity_inside_collision_shape: &VoxelShape,
+ physics: &Physics,
+) -> bool {
+ let bounding_box_from = physics.dimensions.make_bounding_box(from);
+ let delta = to - from;
+ bounding_box_from.collided_along_vector(
+ delta,
+ &entity_inside_collision_shape
+ .move_relative(traversed_block.to_vec3_floored())
+ .to_aabbs(),
+ )
+}
+
+// BlockBehavior.entityInside
+fn handle_entity_inside_block(
+ world: &Instance,
+ block: BlockState,
+ block_pos: BlockPos,
+ physics: &mut Physics,
+) {
+ let registry_block = azalea_registry::Block::from(block);
+ #[allow(clippy::single_match)]
+ match registry_block {
+ azalea_registry::Block::BubbleColumn => {
+ let block_above = world.get_block_state(&block_pos.up(1)).unwrap_or_default();
+ let is_block_above_empty =
+ block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty();
+ let drag_down = block
+ .property::<properties::Drag>()
+ .expect("drag property should always be present on bubble columns");
+ let velocity = &mut physics.velocity;
+
+ if is_block_above_empty {
+ let new_y = if drag_down {
+ f64::max(-0.9, velocity.y - 0.03)
+ } else {
+ f64::min(1.8, velocity.y + 0.1)
+ };
+ velocity.y = new_y;
+ } else {
+ let new_y = if drag_down {
+ f64::max(-0.3, velocity.y - 0.03)
+ } else {
+ f64::min(0.7, velocity.y + 0.06)
+ };
+ velocity.y = new_y;
+ physics.reset_fall_distance();
+ }
+ }
+ _ => {}
+ }
+}
+
+pub struct EntityMovement {
+ pub from: Vec3,
+ pub to: Vec3,
+}
+
pub fn jump_from_ground(
physics: &mut Physics,
position: &Position,
@@ -232,6 +342,12 @@ pub fn jump_from_ground(
physics.has_impulse = true;
}
+pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) {
+ for (mut physics, position) in &mut query {
+ physics.set_old_pos(position);
+ }
+}
+
fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
BlockPos::new(
position.x.floor() as i32,
@@ -241,7 +357,7 @@ fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
)
}
-// opts for handle_relative_friction_and_calculate_movement
+/// Options for [`handle_relative_friction_and_calculate_movement`]
struct HandleRelativeFrictionAndCalculateMovementOpts<'a> {
block_friction: f32,
world: &'a Instance,
@@ -254,7 +370,6 @@ struct HandleRelativeFrictionAndCalculateMovementOpts<'a> {
pose: Option<&'a Pose>,
jumping: &'a Jumping,
}
-
fn handle_relative_friction_and_calculate_movement(
HandleRelativeFrictionAndCalculateMovementOpts {
block_friction,
@@ -274,22 +389,22 @@ fn handle_relative_friction_and_calculate_movement(
direction,
get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting),
&Vec3 {
- x: physics.xxa as f64,
- y: physics.yya as f64,
- z: physics.zza as f64,
+ x: physics.x_acceleration as f64,
+ y: physics.y_acceleration as f64,
+ z: physics.z_acceleration as f64,
},
);
physics.velocity = handle_on_climbable(physics.velocity, on_climbable, &position, world, pose);
move_colliding(
- &MoverType::Own,
+ MoverType::Own,
&physics.velocity.clone(),
world,
&mut position,
physics,
)
- .expect("Entity should exist.");
+ .expect("Entity should exist");
// let delta_movement = entity.delta;
// ladders
// if ((entity.horizontalCollision || entity.jumping) && (entity.onClimbable()
@@ -418,369 +533,3 @@ fn jump_boost_power() -> f64 {
// }
0.
}
-
-#[cfg(test)]
-mod tests {
-
- use azalea_core::{position::ChunkPos, resource_location::ResourceLocation};
- use azalea_entity::{EntityBundle, EntityPlugin};
- use azalea_world::{Chunk, MinecraftEntityId, PartialInstance};
- use uuid::Uuid;
-
- use super::*;
-
- /// You need an app to spawn entities in the world and do updates.
- fn make_test_app() -> App {
- let mut app = App::new();
- app.add_plugins((PhysicsPlugin, EntityPlugin))
- .init_resource::<InstanceContainer>();
- app
- }
-
- #[test]
- fn test_gravity() {
- let mut app = make_test_app();
- let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
- ResourceLocation::new("minecraft:overworld"),
- 384,
- -64,
- );
- let mut partial_world = PartialInstance::default();
- // the entity has to be in a loaded chunk for physics to work
- partial_world.chunks.set(
- &ChunkPos { x: 0, z: 0 },
- Some(Chunk::default()),
- &mut world_lock.write().chunks,
- );
-
- let entity = app
- .world_mut()
- .spawn((
- EntityBundle::new(
- Uuid::nil(),
- Vec3 {
- x: 0.,
- y: 70.,
- z: 0.,
- },
- azalea_registry::EntityKind::Zombie,
- ResourceLocation::new("minecraft:overworld"),
- ),
- MinecraftEntityId(0),
- LocalEntity,
- ))
- .id();
- {
- let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
- // y should start at 70
- assert_eq!(entity_pos.y, 70.);
- }
- app.update();
- app.world_mut().run_schedule(GameTick);
- app.update();
- {
- let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
- // delta is applied before gravity, so the first tick only sets the delta
- assert_eq!(entity_pos.y, 70.);
- let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
- assert!(entity_physics.velocity.y < 0.);
- }
- app.world_mut().run_schedule(GameTick);
- app.update();
- {
- let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
- // the second tick applies the delta to the position, so now it should go down
- assert!(
- entity_pos.y < 70.,
- "Entity y ({}) didn't go down after physics steps",
- entity_pos.y
- );
- }
- }
- #[test]
- fn test_collision() {
- let mut app = make_test_app();
- let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
- ResourceLocation::new("minecraft:overworld"),
- 384,
- -64,
- );
- let mut partial_world = PartialInstance::default();
-
- partial_world.chunks.set(
- &ChunkPos { x: 0, z: 0 },
- Some(Chunk::default()),
- &mut world_lock.write().chunks,
- );
- let entity = app
- .world_mut()
- .spawn((
- EntityBundle::new(
- Uuid::nil(),
- Vec3 {
- x: 0.5,
- y: 70.,
- z: 0.5,
- },
- azalea_registry::EntityKind::Player,
- ResourceLocation::new("minecraft:overworld"),
- ),
- MinecraftEntityId(0),
- LocalEntity,
- ))
- .id();
- let block_state = partial_world.chunks.set_block_state(
- &BlockPos { x: 0, y: 69, z: 0 },
- azalea_registry::Block::Stone.into(),
- &world_lock.write().chunks,
- );
- assert!(
- block_state.is_some(),
- "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
- );
- app.update();
- app.world_mut().run_schedule(GameTick);
- app.update();
- {
- let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
- // delta will change, but it won't move until next tick
- assert_eq!(entity_pos.y, 70.);
- let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
- assert!(entity_physics.velocity.y < 0.);
- }
- app.world_mut().run_schedule(GameTick);
- app.update();
- {
- let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
- // the second tick applies the delta to the position, but it also does collision
- assert_eq!(entity_pos.y, 70.);
- }
- }
-
- #[test]
- fn test_slab_collision() {
- let mut app = make_test_app();
- let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
- ResourceLocation::new("minecraft:overworld"),
- 384,
- -64,
- );
- let mut partial_world = PartialInstance::default();
-
- partial_world.chunks.set(
- &ChunkPos { x: 0, z: 0 },
- Some(Chunk::default()),
- &mut world_lock.write().chunks,
- );
- let entity = app
- .world_mut()
- .spawn((
- EntityBundle::new(
- Uuid::nil(),
- Vec3 {
- x: 0.5,
- y: 71.,
- z: 0.5,
- },
- azalea_registry::EntityKind::Player,
- ResourceLocation::new("minecraft:overworld"),
- ),
- MinecraftEntityId(0),
- LocalEntity,
- ))
- .id();
- let block_state = partial_world.chunks.set_block_state(
- &BlockPos { x: 0, y: 69, z: 0 },
- azalea_block::blocks::StoneSlab {
- kind: azalea_block::properties::Type::Bottom,
- waterlogged: false,
- }
- .into(),
- &world_lock.write().chunks,
- );
- assert!(
- block_state.is_some(),
- "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
- );
- // do a few steps so we fall on the slab
- for _ in 0..20 {
- app.world_mut().run_schedule(GameTick);
- app.update();
- }
- let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
- assert_eq!(entity_pos.y, 69.5);
- }
-
- #[test]
- fn test_top_slab_collision() {
- let mut app = make_test_app();
- let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
- ResourceLocation::new("minecraft:overworld"),
- 384,
- -64,
- );
- let mut partial_world = PartialInstance::default();
-
- partial_world.chunks.set(
- &ChunkPos { x: 0, z: 0 },
- Some(Chunk::default()),
- &mut world_lock.write().chunks,
- );
- let entity = app
- .world_mut()
- .spawn((
- EntityBundle::new(
- Uuid::nil(),
- Vec3 {
- x: 0.5,
- y: 71.,
- z: 0.5,
- },
- azalea_registry::EntityKind::Player,
- ResourceLocation::new("minecraft:overworld"),
- ),
- MinecraftEntityId(0),
- LocalEntity,
- ))
- .id();
- let block_state = world_lock.write().chunks.set_block_state(
- &BlockPos { x: 0, y: 69, z: 0 },
- azalea_block::blocks::StoneSlab {
- kind: azalea_block::properties::Type::Top,
- waterlogged: false,
- }
- .into(),
- );
- assert!(
- block_state.is_some(),
- "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
- );
- // do a few steps so we fall on the slab
- for _ in 0..20 {
- app.world_mut().run_schedule(GameTick);
- app.update();
- }
- let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
- assert_eq!(entity_pos.y, 70.);
- }
-
- #[test]
- fn test_weird_wall_collision() {
- let mut app = make_test_app();
- let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
- ResourceLocation::new("minecraft:overworld"),
- 384,
- -64,
- );
- let mut partial_world = PartialInstance::default();
-
- partial_world.chunks.set(
- &ChunkPos { x: 0, z: 0 },
- Some(Chunk::default()),
- &mut world_lock.write().chunks,
- );
- let entity = app
- .world_mut()
- .spawn((
- EntityBundle::new(
- Uuid::nil(),
- Vec3 {
- x: 0.5,
- y: 73.,
- z: 0.5,
- },
- azalea_registry::EntityKind::Player,
- ResourceLocation::new("minecraft:overworld"),
- ),
- MinecraftEntityId(0),
- LocalEntity,
- ))
- .id();
- let block_state = world_lock.write().chunks.set_block_state(
- &BlockPos { x: 0, y: 69, z: 0 },
- azalea_block::blocks::CobblestoneWall {
- east: azalea_block::properties::WallEast::Low,
- north: azalea_block::properties::WallNorth::Low,
- south: azalea_block::properties::WallSouth::Low,
- west: azalea_block::properties::WallWest::Low,
- up: false,
- waterlogged: false,
- }
- .into(),
- );
- assert!(
- block_state.is_some(),
- "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
- );
- // do a few steps so we fall on the wall
- for _ in 0..20 {
- app.world_mut().run_schedule(GameTick);
- app.update();
- }
-
- let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
- assert_eq!(entity_pos.y, 70.5);
- }
-
- #[test]
- fn test_negative_coordinates_weird_wall_collision() {
- let mut app = make_test_app();
- let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
- ResourceLocation::new("minecraft:overworld"),
- 384,
- -64,
- );
- let mut partial_world = PartialInstance::default();
-
- partial_world.chunks.set(
- &ChunkPos { x: -1, z: -1 },
- Some(Chunk::default()),
- &mut world_lock.write().chunks,
- );
- let entity = app
- .world_mut()
- .spawn((
- EntityBundle::new(
- Uuid::nil(),
- Vec3 {
- x: -7.5,
- y: 73.,
- z: -7.5,
- },
- azalea_registry::EntityKind::Player,
- ResourceLocation::new("minecraft:overworld"),
- ),
- MinecraftEntityId(0),
- LocalEntity,
- ))
- .id();
- let block_state = world_lock.write().chunks.set_block_state(
- &BlockPos {
- x: -8,
- y: 69,
- z: -8,
- },
- azalea_block::blocks::CobblestoneWall {
- east: azalea_block::properties::WallEast::Low,
- north: azalea_block::properties::WallNorth::Low,
- south: azalea_block::properties::WallSouth::Low,
- west: azalea_block::properties::WallWest::Low,
- up: false,
- waterlogged: false,
- }
- .into(),
- );
- assert!(
- block_state.is_some(),
- "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
- );
- // do a few steps so we fall on the wall
- for _ in 0..20 {
- app.world_mut().run_schedule(GameTick);
- app.update();
- }
-
- let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
- assert_eq!(entity_pos.y, 70.5);
- }
-}
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<LocalEntity>, With<InLoadedChunk>),
+ >,
+ instance_container: Res<InstanceContainer>,
+) {
+ for (
+ mut physics,
+ direction,
+ position,
+ sprinting,
+ pose,
+ attributes,
+ world_name,
+ on_climbable,
+ jumping,
+ ) in &mut query
+ {
+ let Some(world_lock) = instance_container.get(world_name) else {
+ continue;
+ };
+ let world = world_lock.read();
+
+ let sprinting = *sprinting.unwrap_or(&Sprinting(false));
+
+ // TODO: elytras
+
+ if physics.is_in_water() || physics.is_in_lava() {
+ // minecraft also checks for `this.isAffectedByFluids() &&
+ // !this.canStandOnFluid(fluidAtBlock)` here but it doesn't matter
+ // for players
+ travel_in_fluid(
+ &mut physics,
+ &direction,
+ position,
+ attributes,
+ sprinting,
+ on_climbable,
+ &world,
+ );
+ } else {
+ travel_in_air(
+ &mut physics,
+ &direction,
+ position,
+ attributes,
+ sprinting,
+ on_climbable,
+ pose,
+ jumping,
+ &world,
+ );
+ }
+ }
+}
+
+/// The usual movement when we're not in water or using an elytra.
+#[allow(clippy::too_many_arguments)]
+fn travel_in_air(
+ physics: &mut Physics,
+ direction: &LookDirection,
+ position: Mut<Position>,
+ attributes: &Attributes,
+ sprinting: Sprinting,
+ on_climbable: &OnClimbable,
+ pose: Option<&Pose>,
+ jumping: &Jumping,
+ world: &Instance,
+) {
+ let gravity = get_effective_gravity();
+
+ let block_pos_below = get_block_pos_below_that_affects_movement(&position);
+
+ let block_state_below = world
+ .chunks
+ .get_block_state(&block_pos_below)
+ .unwrap_or(BlockState::AIR);
+ let block_below: Box<dyn Block> = block_state_below.into();
+ let block_friction = block_below.behavior().friction;
+
+ let inertia = if physics.on_ground() {
+ block_friction * 0.91
+ } else {
+ 0.91
+ };
+
+ // this applies the current delta
+ let mut movement = handle_relative_friction_and_calculate_movement(
+ HandleRelativeFrictionAndCalculateMovementOpts {
+ block_friction,
+ world,
+ physics,
+ direction,
+ position,
+ attributes,
+ is_sprinting: *sprinting,
+ on_climbable,
+ pose,
+ jumping,
+ },
+ );
+
+ movement.y -= gravity;
+
+ // if (this.shouldDiscardFriction()) {
+ // this.setDeltaMovement(movement.x, yMovement, movement.z);
+ // } else {
+ // this.setDeltaMovement(movement.x * (double)inertia, yMovement *
+ // 0.9800000190734863D, movement.z * (double)inertia); }
+
+ // if should_discard_friction(self) {
+ if false {
+ physics.velocity = movement;
+ } else {
+ physics.velocity = Vec3 {
+ x: movement.x * inertia as f64,
+ y: movement.y * 0.9800000190734863f64,
+ z: movement.z * inertia as f64,
+ };
+ }
+}
+
+fn travel_in_fluid(
+ physics: &mut Physics,
+ direction: &LookDirection,
+ mut position: Mut<Position>,
+ attributes: &Attributes,
+ sprinting: Sprinting,
+ on_climbable: &OnClimbable,
+ world: &Instance,
+) {
+ let moving_down = physics.velocity.y <= 0.;
+ let y = position.y;
+ let gravity = get_effective_gravity();
+
+ let acceleration = Vec3::new(
+ physics.x_acceleration as f64,
+ physics.y_acceleration as f64,
+ physics.z_acceleration as f64,
+ );
+
+ if physics.was_touching_water {
+ let mut water_movement_speed = if *sprinting { 0.9 } else { 0.8 };
+ let mut speed = 0.02;
+ let mut water_efficiency_modifier = attributes.water_movement_efficiency.calculate() as f32;
+ if !physics.on_ground() {
+ water_efficiency_modifier *= 0.5;
+ }
+
+ if water_efficiency_modifier > 0. {
+ water_movement_speed += (0.54600006 - water_movement_speed) * water_efficiency_modifier;
+ speed += (attributes.speed.calculate() as f32 - speed) * water_efficiency_modifier;
+ }
+
+ // if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) {
+ // waterMovementSpeed = 0.96F;
+ // }
+
+ move_relative(physics, direction, speed, &acceleration);
+ move_colliding(
+ MoverType::Own,
+ &physics.velocity.clone(),
+ world,
+ &mut position,
+ physics,
+ )
+ .expect("Entity should exist");
+
+ let mut new_velocity = physics.velocity;
+ if physics.horizontal_collision && **on_climbable {
+ // underwater ladders
+ new_velocity.y = 0.2;
+ }
+ new_velocity.x *= water_movement_speed as f64;
+ new_velocity.y *= 0.8;
+ new_velocity.z *= water_movement_speed as f64;
+ physics.velocity =
+ get_fluid_falling_adjusted_movement(gravity, moving_down, new_velocity, sprinting);
+ } else {
+ move_relative(physics, direction, 0.02, &acceleration);
+ move_colliding(
+ MoverType::Own,
+ &physics.velocity.clone(),
+ world,
+ &mut position,
+ physics,
+ )
+ .expect("Entity should exist");
+
+ if physics.lava_fluid_height <= fluid_jump_threshold() {
+ physics.velocity.x *= 0.5;
+ physics.velocity.y *= 0.8;
+ physics.velocity.z *= 0.5;
+ let new_velocity = get_fluid_falling_adjusted_movement(
+ gravity,
+ moving_down,
+ physics.velocity,
+ sprinting,
+ );
+ physics.velocity = new_velocity;
+ } else {
+ physics.velocity *= 0.5;
+ }
+
+ if gravity != 0.0 {
+ physics.velocity.y -= gravity / 4.0;
+ }
+ }
+
+ let velocity = physics.velocity;
+ if physics.horizontal_collision
+ && is_free(
+ physics.bounding_box,
+ world,
+ velocity.x,
+ velocity.y + 0.6 - position.y + y,
+ velocity.z,
+ )
+ {
+ physics.velocity.y = 0.3;
+ }
+}
+
+fn get_fluid_falling_adjusted_movement(
+ gravity: f64,
+ moving_down: bool,
+ new_velocity: Vec3,
+ sprinting: Sprinting,
+) -> Vec3 {
+ if gravity != 0. && !*sprinting {
+ let new_y_velocity = if moving_down
+ && (new_velocity.y - 0.005).abs() >= 0.003
+ && f64::abs(new_velocity.y - gravity / 16.0) < 0.003
+ {
+ -0.003
+ } else {
+ new_velocity.y - gravity / 16.0
+ };
+
+ Vec3 {
+ x: new_velocity.x,
+ y: new_y_velocity,
+ z: new_velocity.z,
+ }
+ } else {
+ new_velocity
+ }
+}
+
+fn is_free(bounding_box: AABB, world: &Instance, x: f64, y: f64, z: f64) -> bool {
+ // let bounding_box = bounding_box.move_relative(Vec3::new(x, y, z));
+
+ let _ = (bounding_box, world, x, y, z);
+
+ // TODO: implement this, see Entity.isFree
+
+ true
+}
+
+fn get_effective_gravity() -> f64 {
+ // TODO: slow falling effect
+ 0.08
+}
+
+pub fn fluid_jump_threshold() -> f64 {
+ // this is 0.0 for entities with an eye height lower than 0.4, but that's not
+ // implemented since it's usually not relevant for players (unless the player
+ // was shrunk)
+ 0.4
+}
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::<InstanceContainer>();
+ app
+}
+
+#[test]
+fn test_gravity() {
+ let mut app = make_test_app();
+ let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
+ ResourceLocation::new("minecraft:overworld"),
+ 384,
+ -64,
+ );
+ let mut partial_world = PartialInstance::default();
+ // the entity has to be in a loaded chunk for physics to work
+ partial_world.chunks.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ &mut world_lock.write().chunks,
+ );
+
+ let entity = app
+ .world_mut()
+ .spawn((
+ EntityBundle::new(
+ Uuid::nil(),
+ Vec3 {
+ x: 0.,
+ y: 70.,
+ z: 0.,
+ },
+ azalea_registry::EntityKind::Zombie,
+ ResourceLocation::new("minecraft:overworld"),
+ ),
+ MinecraftEntityId(0),
+ LocalEntity,
+ ))
+ .id();
+ {
+ let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
+ // y should start at 70
+ assert_eq!(entity_pos.y, 70.);
+ }
+ app.update();
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ {
+ let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
+ // delta is applied before gravity, so the first tick only sets the delta
+ assert_eq!(entity_pos.y, 70.);
+ let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
+ assert!(entity_physics.velocity.y < 0.);
+ }
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ {
+ let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
+ // the second tick applies the delta to the position, so now it should go down
+ assert!(
+ entity_pos.y < 70.,
+ "Entity y ({}) didn't go down after physics steps",
+ entity_pos.y
+ );
+ }
+}
+#[test]
+fn test_collision() {
+ let mut app = make_test_app();
+ let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
+ ResourceLocation::new("minecraft:overworld"),
+ 384,
+ -64,
+ );
+ let mut partial_world = PartialInstance::default();
+
+ partial_world.chunks.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ &mut world_lock.write().chunks,
+ );
+ let entity = app
+ .world_mut()
+ .spawn((
+ EntityBundle::new(
+ Uuid::nil(),
+ Vec3 {
+ x: 0.5,
+ y: 70.,
+ z: 0.5,
+ },
+ azalea_registry::EntityKind::Player,
+ ResourceLocation::new("minecraft:overworld"),
+ ),
+ MinecraftEntityId(0),
+ LocalEntity,
+ ))
+ .id();
+ let block_state = partial_world.chunks.set_block_state(
+ &BlockPos { x: 0, y: 69, z: 0 },
+ azalea_registry::Block::Stone.into(),
+ &world_lock.write().chunks,
+ );
+ assert!(
+ block_state.is_some(),
+ "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
+ );
+ app.update();
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ {
+ let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
+ // delta will change, but it won't move until next tick
+ assert_eq!(entity_pos.y, 70.);
+ let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
+ assert!(entity_physics.velocity.y < 0.);
+ }
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ {
+ let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
+ // the second tick applies the delta to the position, but it also does collision
+ assert_eq!(entity_pos.y, 70.);
+ }
+}
+
+#[test]
+fn test_slab_collision() {
+ let mut app = make_test_app();
+ let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
+ ResourceLocation::new("minecraft:overworld"),
+ 384,
+ -64,
+ );
+ let mut partial_world = PartialInstance::default();
+
+ partial_world.chunks.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ &mut world_lock.write().chunks,
+ );
+ let entity = app
+ .world_mut()
+ .spawn((
+ EntityBundle::new(
+ Uuid::nil(),
+ Vec3 {
+ x: 0.5,
+ y: 71.,
+ z: 0.5,
+ },
+ azalea_registry::EntityKind::Player,
+ ResourceLocation::new("minecraft:overworld"),
+ ),
+ MinecraftEntityId(0),
+ LocalEntity,
+ ))
+ .id();
+ let block_state = partial_world.chunks.set_block_state(
+ &BlockPos { x: 0, y: 69, z: 0 },
+ azalea_block::blocks::StoneSlab {
+ kind: azalea_block::properties::Type::Bottom,
+ waterlogged: false,
+ }
+ .into(),
+ &world_lock.write().chunks,
+ );
+ assert!(
+ block_state.is_some(),
+ "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
+ );
+ // do a few steps so we fall on the slab
+ for _ in 0..20 {
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ }
+ let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
+ assert_eq!(entity_pos.y, 69.5);
+}
+
+#[test]
+fn test_top_slab_collision() {
+ let mut app = make_test_app();
+ let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
+ ResourceLocation::new("minecraft:overworld"),
+ 384,
+ -64,
+ );
+ let mut partial_world = PartialInstance::default();
+
+ partial_world.chunks.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ &mut world_lock.write().chunks,
+ );
+ let entity = app
+ .world_mut()
+ .spawn((
+ EntityBundle::new(
+ Uuid::nil(),
+ Vec3 {
+ x: 0.5,
+ y: 71.,
+ z: 0.5,
+ },
+ azalea_registry::EntityKind::Player,
+ ResourceLocation::new("minecraft:overworld"),
+ ),
+ MinecraftEntityId(0),
+ LocalEntity,
+ ))
+ .id();
+ let block_state = world_lock.write().chunks.set_block_state(
+ &BlockPos { x: 0, y: 69, z: 0 },
+ azalea_block::blocks::StoneSlab {
+ kind: azalea_block::properties::Type::Top,
+ waterlogged: false,
+ }
+ .into(),
+ );
+ assert!(
+ block_state.is_some(),
+ "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
+ );
+ // do a few steps so we fall on the slab
+ for _ in 0..20 {
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ }
+ let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
+ assert_eq!(entity_pos.y, 70.);
+}
+
+#[test]
+fn test_weird_wall_collision() {
+ let mut app = make_test_app();
+ let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
+ ResourceLocation::new("minecraft:overworld"),
+ 384,
+ -64,
+ );
+ let mut partial_world = PartialInstance::default();
+
+ partial_world.chunks.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ &mut world_lock.write().chunks,
+ );
+ let entity = app
+ .world_mut()
+ .spawn((
+ EntityBundle::new(
+ Uuid::nil(),
+ Vec3 {
+ x: 0.5,
+ y: 73.,
+ z: 0.5,
+ },
+ azalea_registry::EntityKind::Player,
+ ResourceLocation::new("minecraft:overworld"),
+ ),
+ MinecraftEntityId(0),
+ LocalEntity,
+ ))
+ .id();
+ let block_state = world_lock.write().chunks.set_block_state(
+ &BlockPos { x: 0, y: 69, z: 0 },
+ azalea_block::blocks::CobblestoneWall {
+ east: azalea_block::properties::WallEast::Low,
+ north: azalea_block::properties::WallNorth::Low,
+ south: azalea_block::properties::WallSouth::Low,
+ west: azalea_block::properties::WallWest::Low,
+ up: false,
+ waterlogged: false,
+ }
+ .into(),
+ );
+ assert!(
+ block_state.is_some(),
+ "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
+ );
+ // do a few steps so we fall on the wall
+ for _ in 0..20 {
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ }
+
+ let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
+ assert_eq!(entity_pos.y, 70.5);
+}
+
+#[test]
+fn test_negative_coordinates_weird_wall_collision() {
+ let mut app = make_test_app();
+ let world_lock = app.world_mut().resource_mut::<InstanceContainer>().insert(
+ ResourceLocation::new("minecraft:overworld"),
+ 384,
+ -64,
+ );
+ let mut partial_world = PartialInstance::default();
+
+ partial_world.chunks.set(
+ &ChunkPos { x: -1, z: -1 },
+ Some(Chunk::default()),
+ &mut world_lock.write().chunks,
+ );
+ let entity = app
+ .world_mut()
+ .spawn((
+ EntityBundle::new(
+ Uuid::nil(),
+ Vec3 {
+ x: -7.5,
+ y: 73.,
+ z: -7.5,
+ },
+ azalea_registry::EntityKind::Player,
+ ResourceLocation::new("minecraft:overworld"),
+ ),
+ MinecraftEntityId(0),
+ LocalEntity,
+ ))
+ .id();
+ let block_state = world_lock.write().chunks.set_block_state(
+ &BlockPos {
+ x: -8,
+ y: 69,
+ z: -8,
+ },
+ azalea_block::blocks::CobblestoneWall {
+ east: azalea_block::properties::WallEast::Low,
+ north: azalea_block::properties::WallNorth::Low,
+ south: azalea_block::properties::WallSouth::Low,
+ west: azalea_block::properties::WallWest::Low,
+ up: false,
+ waterlogged: false,
+ }
+ .into(),
+ );
+ assert!(
+ block_state.is_some(),
+ "Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
+ );
+ // do a few steps so we fall on the wall
+ for _ in 0..20 {
+ app.world_mut().run_schedule(GameTick);
+ app.update();
+ }
+
+ let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
+ assert_eq!(entity_pos.y, 70.5);
+}
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<ChunkPos, HashSet<Entity>>,
- /// An index of Minecraft entity IDs to Azalea ECS entities. You should
- /// avoid using this and instead use `azalea_entity::EntityIdIndex`
+ /// An index of Minecraft entity IDs to Azalea ECS entities.
+ ///
+ /// You should avoid using this (particularly if you're using swarms) and
+ /// instead use `azalea_entity::EntityIdIndex`, since some servers may
+ /// give different entity IDs for the same entities to different
+ /// players.
pub entity_by_id: IntMap<MinecraftEntityId, Entity>,
pub registries: RegistryHolder,
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<Mutex<CommandSource>>) {
println!("getblock xyz {x} {y} {z}");
let block_pos = BlockPos::new(x, y, z);
let block = source.bot.world().read().get_block_state(&block_pos);
- source.reply(&format!("Block at {block_pos:?} is {block:?}"));
+ source.reply(&format!("Block at {block_pos} is {block:?}"));
+ 1
+ })),
+ )));
+ commands.register(literal("getfluid").then(argument("x", integer()).then(
+ argument("y", integer()).then(argument("z", integer()).executes(|ctx: &Ctx| {
+ let source = ctx.source.lock();
+ let x = get_integer(ctx, "x").unwrap();
+ let y = get_integer(ctx, "y").unwrap();
+ let z = get_integer(ctx, "z").unwrap();
+ println!("getfluid xyz {x} {y} {z}");
+ let block_pos = BlockPos::new(x, y, z);
+ let block = source.bot.world().read().get_fluid_state(&block_pos);
+ source.reply(&format!("Fluid at {block_pos} is {block:?}"));
1
})),
)));
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(),
}