diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-01-10 16:45:27 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-10 16:45:27 -0600 |
| commit | 0d16f01571ec8315f3979eae46981e559ade1cf9 (patch) | |
| tree | ea43c32a57b0e6a67579d75a134dfbc009d09781 /azalea-block/src | |
| parent | 615d8f9d2ac56b3244d328587243301da253eafd (diff) | |
| download | azalea-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
Diffstat (limited to 'azalea-block/src')
| -rwxr-xr-x | azalea-block/src/behavior.rs | 9 | ||||
| -rw-r--r-- | azalea-block/src/block_state.rs | 156 | ||||
| -rw-r--r-- | azalea-block/src/fluid_state.rs | 137 | ||||
| -rwxr-xr-x | azalea-block/src/lib.rs | 241 | ||||
| -rw-r--r-- | azalea-block/src/range.rs | 6 |
5 files changed, 310 insertions, 239 deletions
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() |
