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

start implementing fluid physics

This commit is contained in:
mat 2025-01-08 22:26:57 +00:00
commit 56e0f61a51
12 changed files with 586 additions and 410 deletions

View file

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

View file

@ -0,0 +1,150 @@
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 {
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)
)
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn Block>::from(value).as_registry_block()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_u32() {
assert_eq!(
BlockState::try_from(0 as BlockStateIntegerRepr).unwrap(),
BlockState::AIR
);
assert!(BlockState::try_from(BlockState::MAX_STATE).is_ok());
assert!(BlockState::try_from(BlockState::MAX_STATE + 1).is_err());
}
#[test]
fn test_from_blockstate() {
let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn Block> =
Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}
#[test]
fn test_debug_blockstate() {
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::FloweringAzalea)
);
assert!(formatted.ends_with(", FloweringAzalea)"), "{}", formatted);
let formatted = format!(
"{:?}",
BlockState::from(azalea_registry::Block::BigDripleafStem)
);
assert!(
formatted.ends_with(", BigDripleafStem { facing: North, waterlogged: false })"),
"{}",
formatted
);
}
}

View file

@ -0,0 +1,103 @@
use crate::block_state::{BlockState, BlockStateIntegerRepr};
#[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.
///
/// 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,
}
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.
}
pub fn get_flow(world: &Instance, pos: BlockPos) {
let _ = world;
let _ = pos;
}
}
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,
}
}
}
}
}
/// Sometimes Minecraft represents fluids with 0 being the empty and 8 being
/// full, and sometimes it's the opposite. You can use this function to convert
/// 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.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,
),
})
}
}
}
}

View file

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

View file

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

View file

@ -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> {
@ -32,13 +27,13 @@ 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 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 x < 0.0 {
min_x -= x;
@ -59,24 +54,19 @@ 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 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 +87,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
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 size(&self) -> f64 {
@ -217,9 +185,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,6 +195,10 @@ 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;
@ -246,7 +218,7 @@ impl AABB {
for aabb in boxes {
dir = Self::get_direction(
&aabb.move_relative(&pos.to_vec3_floored()),
&aabb.move_relative(pos.to_vec3_floored()),
from,
&mut t,
dir,
@ -276,11 +248,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: aabb.min.x,
min_x: aabb.min.y,
max_x: aabb.max.y,
min_z: aabb.min.z,
max_z: aabb.max.z,
result_dir: Direction::West,
start: from,
});
@ -289,11 +261,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: aabb.max.x,
min_x: aabb.min.y,
max_x: aabb.max.y,
min_z: aabb.min.z,
max_z: aabb.max.z,
result_dir: Direction::East,
start: from,
});
@ -308,11 +280,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: aabb.min.y,
min_x: aabb.min.z,
max_x: aabb.max.z,
min_z: aabb.min.x,
max_z: aabb.max.x,
result_dir: Direction::Down,
start: &Vec3 {
x: from.y,
@ -329,11 +301,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: aabb.max.y,
min_x: aabb.min.z,
max_x: aabb.max.z,
min_z: aabb.min.x,
max_z: aabb.max.x,
result_dir: Direction::Up,
start: &Vec3 {
x: from.y,
@ -352,11 +324,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: aabb.min.z,
min_x: aabb.min.x,
max_x: aabb.max.x,
min_z: aabb.min.y,
max_z: aabb.max.y,
result_dir: Direction::North,
start: &Vec3 {
x: from.z,
@ -373,11 +345,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: aabb.max.z,
min_x: aabb.min.x,
max_x: aabb.max.x,
min_z: aabb.min.y,
max_z: aabb.max.y,
result_dir: Direction::South,
start: &Vec3 {
x: from.z,
@ -409,38 +381,42 @@ 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)
}
}
@ -453,12 +429,8 @@ mod tests {
assert_ne!(
AABB::clip_iterable(
&vec![AABB {
min_x: 0.,
min_y: 0.,
min_z: 0.,
max_x: 1.,
max_y: 1.,
max_z: 1.,
min: Vec3::new(0., 0., 0.),
max: Vec3::new(1., 1., 1.),
}],
&Vec3::new(-1., -1., -1.),
&Vec3::new(1., 1., 1.),

View file

@ -11,13 +11,8 @@ impl EntityDimensions {
let radius = (self.width / 2.0) as f64;
let height = self.height as f64;
AABB {
min_x: pos.x - radius,
min_y: pos.y,
min_z: pos.z - radius,
max_x: pos.x + radius,
max_y: pos.y + height,
max_z: pos.z + radius,
min: Vec3::new(pos.x - radius, pos.y, pos.z - radius),
max: Vec3::new(pos.x + radius, pos.y + height, pos.z + radius),
}
}
}

View file

@ -12,6 +12,7 @@ mod plugin;
pub mod vec_delta_codec;
use std::{
collections::HashMap,
fmt::Debug,
hash::{Hash, Hasher},
};
@ -251,7 +252,10 @@ impl Eq for LookDirection {}
#[derive(Debug, Component, Clone, Default)]
pub struct Physics {
/// How fast the entity is moving.
///
/// Sometimes referred to as the delta movement.
pub velocity: Vec3,
pub vec_delta_codec: VecDeltaCodec,
/// X acceleration.
pub xxa: f32,
@ -263,8 +267,6 @@ pub struct Physics {
on_ground: bool,
last_on_ground: bool,
pub vec_delta_codec: VecDeltaCodec,
/// The width and height of the entity.
pub dimensions: EntityDimensions,
/// The bounding box of the entity. This is more than just width and height,
@ -276,12 +278,22 @@ pub struct Physics {
pub horizontal_collision: bool,
// pub minor_horizontal_collision: bool,
pub vertical_collision: bool,
pub water_fluid_height: f64,
pub lava_fluid_height: f64,
pub was_touching_water: bool,
// TODO: implement fall_distance
pub fall_distance: f32,
// TODO: implement remaining_fire_ticks
pub remaining_fire_ticks: i32,
}
impl Physics {
pub fn new(dimensions: EntityDimensions, pos: Vec3) -> Self {
Self {
velocity: Vec3::default(),
vec_delta_codec: VecDeltaCodec::new(pos),
xxa: 0.,
yya: 0.,
@ -298,7 +310,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 +338,13 @@ 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;
}
}
/// Marker component for entities that are dead.

View file

@ -104,7 +104,7 @@ 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;
} else {

View file

@ -0,0 +1,148 @@
use std::cmp;
use azalea_core::position::{BlockPos, Vec3};
use azalea_entity::{metadata::AbstractBoat, InLoadedChunk, LocalEntity, Physics, Position};
use azalea_registry::{EntityKind, Fluid};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_ecs::prelude::*;
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 lava_push_factor = world
.registries
.dimension_type()
.map(|d| d.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, Fluid::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: Fluid,
fluid_push_factor: f32,
) -> 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 max_x = checking_liquids_aabb.max.x.ceil() as i32;
let min_y = checking_liquids_aabb.min.y.floor() as i32;
let max_y = checking_liquids_aabb.max.y.ceil() as i32;
let min_z = checking_liquids_aabb.min.z.floor() 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.fluid != 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::min(
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 = fluid_at_cur_pos.flow();
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;
additionalPlayerDelta *= 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 {
Fluid::Water => physics.water_fluid_height = min_height_touching,
Fluid::Lava => physics.lava_fluid_height = min_height_touching,
checking_fluid => panic!("unknown fluid {checking_fluid}"),
};
touching_fluid
}
pub fn update_swimming() {
// TODO: swimming
}

View file

@ -3,6 +3,7 @@
pub mod clip;
pub mod collision;
pub mod fluids;
use azalea_block::{Block, BlockState};
use azalea_core::{
@ -33,7 +34,13 @@ 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),
fluids::update_swimming.after(azalea_entity::update_fluid_on_eyes),
ai_step,
travel,
)
.chain()
.in_set(PhysicsSet)
.after(azalea_entity::update_in_loaded_chunk),
@ -87,6 +94,10 @@ fn travel(
// TODO: slow falling effect
// let is_falling = self.delta.y <= 0.;
let block_position = BlockPos::from(**position);
// LivingEntity.travel
let fluid_state = world.chunks.get_fluid_state(&block_position);
// TODO: fluids
// TODO: elytra

View file

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