Skip to content

Commit

Permalink
Fluid physics (#199)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mat-1 authored Jan 10, 2025
1 parent 615d8f9 commit 0d16f01
Show file tree
Hide file tree
Showing 38 changed files with 2,173 additions and 1,096 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions azalea-block/azalea-block-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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.
///
Expand Down
9 changes: 9 additions & 0 deletions azalea-block/src/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -14,6 +16,7 @@ impl Default for BlockBehavior {
destroy_time: 0.,
explosion_resistance: 0.,
requires_correct_tool_for_drops: false,
force_solid: None,
}
}
}
Expand Down Expand Up @@ -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
}
}
156 changes: 156 additions & 0 deletions azalea-block/src/block_state.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
}
137 changes: 137 additions & 0 deletions azalea-block/src/fluid_state.rs
Original file line number Diff line number Diff line change
@@ -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),
}),
}
}
}
Loading

0 comments on commit 0d16f01

Please sign in to comment.