Repository: Pumpkin-MC/Pumpkin
Pull request #643: Implement Flowing Fluids
Author: login: teknostom
Created: 2025-03-19T18:12:19Z | Merged: 2025-04-05T09:54:41Z

## Description
This implementation follows the Minecraft vanilla implementation as closely as possible.

Features
[x] Flow from a source.
[x] Dissapate when source is removed.
[x] form new sources when applicable.

## Testing

Please follow our [Coding Guidelines](https://github.com/Pumpkin-MC/Pumpkin/blob/master/CONTRIBUTING.md#coding-guidelines)



Diff:
diff --git a/pumpkin-data/build/fluid.rs b/pumpkin-data/build/fluid.rs
index cfd3f4d7df..5545afdd62 100644
--- a/pumpkin-data/build/fluid.rs
+++ b/pumpkin-data/build/fluid.rs
@@ -1,57 +1,402 @@
-use heck::ToShoutySnakeCase;
-use proc_macro2::TokenStream;
-use quote::{format_ident, quote};
+use heck::{ToShoutySnakeCase, ToUpperCamelCase};
+use proc_macro2::{Span, TokenStream};
+use quote::{ToTokens, format_ident, quote};
 use serde::Deserialize;
-use syn::{LitInt, LitStr};
+use std::collections::{HashMap, HashSet};
+use syn::{Ident, LitInt, LitStr};
 
-#[derive(Deserialize)]
+fn const_fluid_name_from_fluid_name(fluid: &str) -> String {
+    fluid.to_shouty_snake_case()
+}
+
+fn property_group_name_from_derived_name(name: &str) -> String {
+    format!("{}_fluid_properties", name).to_upper_camel_case()
+}
+
+struct PropertyVariantMapping {
+    original_name: String,
+    property_enum: String,
+}
+
+struct PropertyCollectionData {
+    variant_mappings: Vec<PropertyVariantMapping>,
+    fluid_names: Vec<String>,
+}
+
+impl PropertyCollectionData {
+    pub fn add_fluid_name(&mut self, fluid_name: String) {
+        self.fluid_names.push(fluid_name);
+    }
+
+    pub fn from_mappings(variant_mappings: Vec<PropertyVariantMapping>) -> Self {
+        Self {
+            variant_mappings,
+            fluid_names: Vec::new(),
+        }
+    }
+
+    pub fn derive_name(&self) -> String {
+        format!("{}_like", self.fluid_names[0])
+    }
+}
+
+#[derive(Deserialize, Clone, Debug)]
+pub struct PropertyStruct {
+    pub name: String,
+    pub values: Vec<String>,
+}
+
+impl ToTokens for PropertyStruct {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let name = Ident::new(&self.name, Span::call_site());
+
+        let variant_count = self.values.clone().len() as u16;
+        let values_index = (0..self.values.clone().len() as u16).collect::<Vec<_>>();
+
+        let ident_values = self.values.iter().map(|value| {
+            let value_str = if value.chars().all(|c| c.is_numeric()) {
+                format!("L{}", value)
+            } else {
+                value.clone()
+            };
+            Ident::new(&value_str.to_upper_camel_case(), Span::call_site())
+        });
+
+        let values_2 = ident_values.clone();
+        let values_3 = ident_values.clone();
+
+        let from_values = self.values.iter().map(|value| {
+            let value_str = if value.chars().all(|c| c.is_numeric()) {
+                format!("L{}", value)
+            } else {
+                value.clone()
+            };
+            let ident = Ident::new(&value_str.to_upper_camel_case(), Span::call_site());
+            quote! {
+                #value => Self::#ident
+            }
+        });
+        let to_values = self.values.iter().map(|value| {
+            let value_str = if value.chars().all(|c| c.is_numeric()) {
+                format!("L{}", value)
+            } else {
+                value.clone()
+            };
+            let ident = Ident::new(&value_str.to_upper_camel_case(), Span::call_site());
+            quote! {
+                Self::#ident => #value
+            }
+        });
+
+        tokens.extend(quote! {
+            #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+            pub enum #name {
+                #(#ident_values),*
+            }
+
+            impl EnumVariants for #name {
+                fn variant_count() -> u16 {
+                    #variant_count
+                }
+
+                fn to_index(&self) -> u16 {
+                    match self {
+                        #(Self::#values_2 => #values_index),*
+                    }
+                }
+
+                fn from_index(index: u16) -> Self {
+                    match index {
+                        #(#values_index => Self::#values_3,)*
+                        _ => panic!("Invalid index: {}", index),
+                    }
+                }
+
+                fn to_value(&self) -> &str {
+                    match self {
+                        #(#to_values),*
+                    }
+                }
+
+                fn from_value(value: &str) -> Self {
+                    match value {
+                        #(#from_values),*,
+                        _ => panic!("Invalid value: {:?}", value),
+                    }
+                }
+            }
+        });
+    }
+}
+
+struct FluidPropertyStruct {
+    data: PropertyCollectionData,
+}
+
+impl ToTokens for FluidPropertyStruct {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let struct_name = property_group_name_from_derived_name(&self.data.derive_name());
+        let name = Ident::new(&struct_name, Span::call_site());
+
+        let values = self.data.variant_mappings.iter().map(|entry| {
+            let key = Ident::new_raw(&entry.original_name, Span::call_site());
+            let value = Ident::new(&entry.property_enum, Span::call_site());
+
+            quote! {
+                #key: #value
+            }
+        });
+
+        let fluid_names = &self.data.fluid_names;
+
+        let field_names: Vec<_> = self
+            .data
+            .variant_mappings
+            .iter()
+            .rev()
+            .map(|entry| Ident::new_raw(&entry.original_name, Span::call_site()))
+            .collect();
+
+        let field_types: Vec<_> = self
+            .data
+            .variant_mappings
+            .iter()
+            .rev()
+            .map(|entry| Ident::new(&entry.property_enum, Span::call_site()))
+            .collect();
+
+        let to_props_values = self.data.variant_mappings.iter().map(|entry| {
+            let key = &entry.original_name;
+            let key2 = Ident::new_raw(&entry.original_name, Span::call_site());
+
+            quote! {
+                props.push((#key.to_string(), self.#key2.to_value().to_string()));
+            }
+        });
+
+        let from_props_values = self.data.variant_mappings.iter().map(|entry| {
+            let key = &entry.original_name;
+            let key2 = Ident::new_raw(&entry.original_name, Span::call_site());
+            let value = Ident::new(&entry.property_enum, Span::call_site());
+
+            quote! {
+                #key => fluid_props.#key2 = #value::from_value(&value)
+            }
+        });
+
+        tokens.extend(quote! {
+            #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+            pub struct #name {
+                #(pub #values),*
+            }
+
+            impl FluidProperties for #name {
+                #[allow(unused_assignments)]
+                fn to_index(&self) -> u16 {
+                    let mut index = 0;
+                    let mut multiplier = 1;
+
+                    #(
+                        index += self.#field_names.to_index() * multiplier;
+                        multiplier *= #field_types::variant_count();
+                    )*
+
+                    index
+                }
+
+                #[allow(unused_assignments)]
+                fn from_index(mut index: u16) -> Self {
+                    Self {
+                        #(
+                            #field_names: {
+                                let value = index % #field_types::variant_count();
+                                index /= #field_types::variant_count();
+                                #field_types::from_index(value)
+                            }
+                        ),*
+                    }
+                }
+
+                fn to_state_id(&self, fluid: &Fluid) -> u16 {
+                    if ![#(#fluid_names),*].contains(&fluid.name) {
+                        panic!("{} is not a valid fluid for {}", &fluid.name, #struct_name);
+                    }
+
+                    let prop_index = self.to_index();
+                    if prop_index < fluid.states.len() as u16 {
+                        fluid.states[prop_index as usize].block_state_id
+                    } else {
+                        fluid.states[fluid.default_state_index as usize].block_state_id
+                    }
+                }
+
+                fn from_state_id(state_id: u16, fluid: &Fluid) -> Self {
+                    if ![#(#fluid_names),*].contains(&fluid.name) {
+                        panic!("{} is not a valid fluid for {}", &fluid.name, #struct_name);
+                    }
+
+                    for (idx, state) in fluid.states.iter().enumerate() {
+                        if state.block_state_id == state_id {
+                            return Self::from_index(idx as u16);
+                        }
+                    }
+
+                    Self::from_index(fluid.default_state_index)
+                }
+
+                fn default(fluid: &Fluid) -> Self {
+                    if ![#(#fluid_names),*].contains(&fluid.name) {
+                        panic!("{} is not a valid fluid for {}", &fluid.name, #struct_name);
+                    }
+
+                    Self::from_state_id(fluid.default_state_index, fluid)
+                }
+
+                #[allow(clippy::vec_init_then_push)]
+                fn to_props(&self) -> Vec<(String, String)> {
+                    let mut props = vec![];
+
+                    #(#to_props_values)*
+
+                    props
+                }
+
+                fn from_props(props: Vec<(String, String)>, fluid: &Fluid) -> Self {
+                    if ![#(#fluid_names),*].contains(&fluid.name) {
+                        panic!("{} is not a valid fluid for {}", &fluid.name, #struct_name);
+                    }
+
+                    let mut fluid_props = Self::default(fluid);
+
+                    for (key, value) in props {
+                        match key.as_str() {
+                            #(#from_props_values),*,
+                            _ => panic!("Invalid key: {}", key),
+                        }
+                    }
+
+                    fluid_props
+                }
+            }
+        });
+    }
+}
+
+#[derive(Deserialize, Clone)]
 struct FluidState {
     height: f32,
-    level: i32,
+    level: i16,
     is_empty: bool,
     blast_resistance: f32,
     block_state_id: u16,
     is_still: bool,
+    // We'll derive is_source and falling from existing fields instead of requiring them in JSON
+}
+
+#[derive(Clone, Debug)]
+struct FluidStateRef {
+    pub id: u16,
+    pub state_idx: u16,
+}
+impl ToTokens for FluidStateRef {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let id = LitInt::new(&self.id.to_string(), Span::call_site());
+        let state_idx = LitInt::new(&self.state_idx.to_string(), Span::call_site());
+        tokens.extend(quote! {
+            FluidStateRef {
+                id: #id,
+                state_idx: #state_idx,
+            }
+        });
+    }
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Clone)]
 struct Property {
     name: String,
     values: Vec<String>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Clone)]
 struct Fluid {
     name: String,
-    id: u8,
+    id: u16,
     properties: Vec<Property>,
-    default_state_index: usize,
+    default_state_index: u16,
     states: Vec<FluidState>,
+    #[serde(default = "default_flow_speed")]
+    flow_speed: u32,
+    #[serde(default = "default_flow_distance")]
+    flow_distance: u32,
+    #[serde(default)]
+    can_convert_to_source: bool,
+}
+
+fn default_flow_speed() -> u32 {
+    5 // Default to water's speed
+}
+
+fn default_flow_distance() -> u32 {
+    4 // Default to water's distance
 }
 
 pub(crate) fn build() -> TokenStream {
     println!("cargo:rerun-if-changed=../assets/fluids.json");
 
-    let fluids: Vec<Fluid> = serde_json::from_str(include_str!("../../assets/fluids.json"))
-        .expect("Failed to parse fluids.json");
+    let fluids: Vec<Fluid> = match serde_json::from_str(include_str!("../../assets/fluids.json")) {
+        Ok(fluids) => fluids,
+        Err(e) => panic!("Failed to parse fluids.json: {}", e),
+    };
 
     let mut constants = TokenStream::new();
     let mut id_matches = Vec::new();
-    for fluid in fluids {
+    let mut type_from_name = TokenStream::new();
+    let mut type_from_raw_id_arms = TokenStream::new();
+    let mut fluid_from_state_id = TokenStream::new();
+
+    let mut fluid_properties_from_state_and_name = TokenStream::new();
+    let mut fluid_properties_from_props_and_name = TokenStream::new();
+
+    // Used to create property `enum`s.
+    let mut property_enums: HashMap<String, PropertyStruct> = HashMap::new();
+    // Property implementation for a fluid.
+    let mut fluid_properties: Vec<FluidPropertyStruct> = Vec::new();
+    // Mapping of a collection of property names -> fluids that have these properties.
+    let mut property_collection_map: HashMap<Vec<String>, PropertyCollectionData> = HashMap::new();
+    // Validator that we have no `enum` collisions.
+    let mut enum_to_values: HashMap<String, Vec<String>> = HashMap::new();
+
+    // Collect unique fluid states to create partial states
+    let mut unique_states = Vec::new();
+    let mut optimized_fluids: Vec<(String, FluidStateRef)> = Vec::new();
+
+    for fluid in fluids.clone() {
         let id_name = LitStr::new(&fluid.name, proc_macro2::Span::call_site());
         let const_ident = format_ident!("{}", fluid.name.to_shouty_snake_case());
+        let state_id_start = fluid
+            .states
+            .iter()
+            .map(|state| state.block_state_id)
+            .min()
+            .unwrap();
+        let state_id_end = fluid
+            .states
+            .iter()
+            .map(|state| state.block_state_id)
+            .max()
+            .unwrap();
 
         let id_lit = LitInt::new(&fluid.id.to_string(), proc_macro2::Span::call_site());
         let mut properties = TokenStream::new();
         if fluid.properties.is_empty() {
             properties.extend(quote!(None));
         } else {
-            let internal_properties = fluid.properties.into_iter().map(|property| {
+            let internal_properties = fluid.properties.iter().map(|property| {
                 let key = LitStr::new(&property.name, proc_macro2::Span::call_site());
                 let values = property
                     .values
-                    .into_iter()
-                    .map(|value| LitStr::new(&value, proc_macro2::Span::call_site()));
+                    .iter()
+                    .map(|value| LitStr::new(value, proc_macro2::Span::call_site()));
 
                 quote! {
                     (#key, &[
@@ -66,13 +411,60 @@ pub(crate) fn build() -> TokenStream {
             });
         }
 
-        let fluid_states = fluid.states.into_iter().map(|state| {
+        for (idx, state) in fluid.states.iter().enumerate() {
+            // Check if this state is already in `unique_states` by comparing key fields
+            let already_exists = unique_states.iter().any(|s: &FluidState| {
+                s.height == state.height
+                    && s.level == state.level
+                    && s.is_empty == state.is_empty
+                    && s.blast_resistance == state.blast_resistance
+                    && s.is_still == state.is_still
+            });
+            if !already_exists {
+                unique_states.push(state.clone());
+            }
+            // Create a reference to the state
+            let state_idx = unique_states
+                .iter()
+                .position(|s| {
+                    s.height == state.height
+                        && s.level == state.level
+                        && s.is_empty == state.is_empty
+                        && s.blast_resistance == state.blast_resistance
+                        && s.is_still == state.is_still
+                })
+                .unwrap() as u16;
+            optimized_fluids.push((
+                fluid.name.clone(),
+                FluidStateRef {
+                    id: idx as u16,
+                    state_idx,
+                },
+            ));
+        }
+        fluid_from_state_id.extend(quote! {
+            #state_id_start..=#state_id_end => Some(Fluid::#const_ident),
+        });
+
+        type_from_name.extend(quote! {
+            #id_name => Some(Self::#const_ident),
+        });
+
+        type_from_raw_id_arms.extend(quote! {
+            #id_lit => Some(Self::#const_ident),
+        });
+
+        let fluid_states = fluid.states.iter().map(|state| {
             let height = state.height;
             let level = state.level;
             let is_empty = state.is_empty;
             let blast_resistance = state.blast_resistance;
             let block_state_id = state.block_state_id;
             let is_still = state.is_still;
+            // Derive these values based on existing fields
+            let is_source = level == 0 && is_still; // Level 0 and still means it's a source
+            let falling = false; // Default to false - we'll handle falling in the fluid behavior code
+
             quote! {
                 FluidState {
                     height: #height,
@@ -81,10 +473,15 @@ pub(crate) fn build() -> TokenStream {
                     blast_resistance: #blast_resistance,
                     block_state_id: #block_state_id,
                     is_still: #is_still,
+                    is_source: #is_source,
+                    falling: #falling,
                 }
             }
         });
-        let state_id = fluid.default_state_index as u8;
+        let state_id = fluid.default_state_index;
+        let flow_speed = fluid.flow_speed;
+        let flow_distance = fluid.flow_distance;
+        let can_convert_to_source = fluid.can_convert_to_source;
 
         id_matches.push(quote! {
             #id_name => Some(#id_lit),
@@ -93,40 +490,293 @@ pub(crate) fn build() -> TokenStream {
         constants.extend(quote! {
             pub const #const_ident: Fluid = Fluid {
                 id: #id_lit,
+                name: #id_name,
                 properties: #properties,
                 states: &[#(#fluid_states),*],
-                default_state_index: #state_id
+                default_state_index: #state_id,
+                flow_speed: #flow_speed,
+                flow_distance: #flow_distance,
+                can_convert_to_source: #can_convert_to_source,
+            };
+        });
+
+        let mut property_collection = HashSet::new();
+        let mut property_mapping = Vec::new();
+        for property in &fluid.properties {
+            property_collection.insert(property.name.clone());
+
+            // Get mapped property `enum` name
+            let renamed_property = property.name.to_upper_camel_case();
+
+            let expected_values = enum_to_values
+                .entry(renamed_property.clone())
+                .or_insert_with(|| property.values.clone());
+
+            if expected_values != &property.values {
+                panic!(
+                    "Enum overlap for '{}' ({:?} vs {:?})",
+                    property.name, &property.values, expected_values
+                );
             };
 
+            property_mapping.push(PropertyVariantMapping {
+                original_name: property.name.clone(),
+                property_enum: renamed_property.clone(),
+            });
+
+            // If this property doesn't have an `enum` yet, make one.
+            let _ = property_enums
+                .entry(renamed_property.clone())
+                .or_insert_with(|| PropertyStruct {
+                    name: renamed_property,
+                    values: property.values.clone(),
+                });
+        }
+
+        if !property_collection.is_empty() {
+            let mut property_collection = Vec::from_iter(property_collection);
+            property_collection.sort();
+            property_collection_map
+                .entry(property_collection)
+                .or_insert_with(|| PropertyCollectionData::from_mappings(property_mapping))
+                .add_fluid_name(fluid.name.clone());
+        }
+    }
+
+    let unique_fluid_states = unique_states.iter().map(|state| {
+        let height = state.height;
+        let level = state.level;
+        let is_empty = state.is_empty;
+        let blast_resistance = state.blast_resistance;
+        let block_state_id = state.block_state_id;
+        let is_still = state.is_still;
+        let is_source = level == 0 && is_still;
+        let falling = false;
+        quote! {
+            PartialFluidState {
+                height: #height,
+                level: #level,
+                is_empty: #is_empty,
+                blast_resistance: #blast_resistance,
+                block_state_id: #block_state_id,
+                is_still: #is_still,
+                is_source: #is_source,
+                falling: #falling,
+            }
+        }
+    });
+
+    for property_group in property_collection_map.into_values() {
+        for fluid_name in &property_group.fluid_names {
+            let const_fluid_name = Ident::new(
+                &const_fluid_name_from_fluid_name(fluid_name),
+                Span::call_site(),
+            );
+            let property_name = Ident::new(
+                &property_group_name_from_derived_name(&property_group.derive_name()),
+                Span::call_site(),
+            );
+
+            fluid_properties_from_state_and_name.extend(quote! {
+                #fluid_name => Some(Box::new(#property_name::from_state_id(state_id, &Fluid::#const_fluid_name))),
+            });
+
+            fluid_properties_from_props_and_name.extend(quote! {
+                #fluid_name => Some(Box::new(#property_name::from_props(props, &Fluid::#const_fluid_name))),
+            });
+        }
+
+        fluid_properties.push(FluidPropertyStruct {
+            data: property_group,
         });
     }
 
+    let fluid_props = fluid_properties.iter().map(|prop| prop.to_token_stream());
+    let properties = property_enums.values().map(|prop| prop.to_token_stream());
+
     quote! {
+        use crate::tag::{Tagable, RegistryKey};
+
+        #[derive(Clone, Debug)]
+        pub struct PartialFluidState {
+            pub height: f32,
+            pub level: i16,
+            pub is_empty: bool,
+            pub blast_resistance: f32,
+            pub block_state_id: u16,
+            pub is_still: bool,
+            pub is_source: bool,
+            pub falling: bool,
+        }
+
+        #[derive(Clone, Debug)]
         pub struct FluidState {
             pub height: f32,
-            pub level: i32,
+            pub level: i16,
             pub is_empty: bool,
             pub blast_resistance: f32,
             pub block_state_id: u16,
             pub is_still: bool,
+            pub is_source: bool,
+            pub falling: bool,
         }
 
+        #[derive(Clone, Debug)]
+        pub struct FluidStateRef {
+            pub id: u16,
+            pub state_idx: u16,
+        }
+
+        #[derive(Clone, Debug)]
         pub struct Fluid {
-            pub id: u8,
+            pub id: u16,
+            pub name: &'static str,
             pub properties: Option<&'static [(&'static str, &'static [&'static str])]>,
             pub states: &'static [FluidState],
-            pub default_state_index: u8,
+            pub default_state_index: u16,
+            pub flow_speed: u32,
+            pub flow_distance: u32,
+            pub can_convert_to_source: bool,
+        }
+
+        pub static FLUID_STATES: &[PartialFluidState] = &[
+            #(#unique_fluid_states),*
+        ];
+
+        pub trait EnumVariants {
+            fn variant_count() -> u16;
+            fn to_index(&self) -> u16;
+            fn from_index(index: u16) -> Self;
+            fn to_value(&self) -> &str;
+            fn from_value(value: &str) -> Self;
+        }
+
+        pub trait FluidProperties where Self: 'static {
+            // Convert properties to an index (`0` to `N-1`).
+            fn to_index(&self) -> u16;
+            // Convert an index back to properties.
+            fn from_index(index: u16) -> Self where Self: Sized;
+
+            // Convert properties to a state id.
+            fn to_state_id(&self, fluid: &Fluid) -> u16;
+            // Convert a state id back to properties.
+            fn from_state_id(state_id: u16, fluid: &Fluid) -> Self where Self: Sized;
+            // Get the default properties.
+            fn default(fluid: &Fluid) -> Self where Self: Sized;
+
+            // Convert properties to a `Vec` of `(name, value)`
+            fn to_props(&self) -> Vec<(String, String)>;
+
+            // Convert properties to a fluid state, and add them onto the default state.
+            fn from_props(props: Vec<(String, String)>, fluid: &Fluid) -> Self where Self: Sized;
         }
 
         impl Fluid {
             #constants
 
+            pub fn from_registry_key(name: &str) -> Option<Self> {
+                match name {
+                    #type_from_name
+                    _ => None
+                }
+            }
+
+            pub const fn from_id(id: u16) -> Option<Self> {
+                match id {
+                    #type_from_raw_id_arms
+                    _ => None
+                }
+            }
+            #[allow(unreachable_patterns)]
+            pub const fn from_state_id(id: u16) -> Option<Self> {
+                match id {
+                    #fluid_from_state_id
+                    _ => None
+                }
+            }
+
+
             pub fn ident_to_fluid_id(name: &str) -> Option<u8> {
                 match name {
                     #(#id_matches)*
                     _ => None
                 }
             }
+
+            #[doc = r" Get the properties of the fluid."]
+            pub fn properties(&self, state_id: u16) -> Option<Box<dyn FluidProperties>> {
+                match self.name {
+                    #fluid_properties_from_state_and_name
+                    _ => None
+                }
+            }
+
+            #[doc = r" Get the properties of the fluid."]
+            pub fn from_properties(&self, props: Vec<(String, String)>) -> Option<Box<dyn FluidProperties>> {
+                match self.name {
+                    #fluid_properties_from_props_and_name
+                    _ => None
+                }
+            }
+
+            // Added helper methods for fluid behavior
+            pub fn is_source(&self, state_id: u16) -> bool {
+                let idx = (state_id as usize) % self.states.len();
+                self.states[idx].is_source
+            }
+
+            pub fn is_falling(&self, state_id: u16) -> bool {
+                let idx = (state_id as usize) % self.states.len();
+                self.states[idx].falling
+            }
+
+            pub fn get_level(&self, state_id: u16) -> i16 {
+                let idx = (state_id as usize) % self.states.len();
+                self.states[idx].level
+            }
+
+            pub fn get_height(&self, state_id: u16) -> f32 {
+                let idx = (state_id as usize) % self.states.len();
+                self.states[idx].height
+            }
+        }
+
+        impl FluidStateRef {
+            pub fn get_state(&self) -> FluidState {
+                let partial_state = &FLUID_STATES[self.state_idx as usize];
+                FluidState {
+                    height: partial_state.height,
+                    level: partial_state.level,
+                    is_empty: partial_state.is_empty,
+                    blast_resistance: partial_state.blast_resistance,
+                    block_state_id: partial_state.block_state_id,
+                    is_still: partial_state.is_still,
+                    is_source: partial_state.is_source,
+                    falling: partial_state.falling,
+                }
+            }
         }
+
+        impl Tagable for Fluid {
+            #[inline]
+            fn tag_key() -> RegistryKey {
+                RegistryKey::Fluid
+            }
+
+            #[inline]
+            fn registry_key(&self) -> &str {
+                self.name
+            }
+        }
+
+        // Added FluidLevel enum and required constants
+        pub const FLUID_LEVEL_SOURCE: i32 = 0;
+        pub const FLUID_LEVEL_FLOWING_MAX: i32 = 8;
+        pub const FLUID_MIN_HEIGHT: f32 = 0.0;
+        pub const FLUID_MAX_HEIGHT: f32 = 1.0;
+
+        #(#properties)*
+
+        #(#fluid_props)*
     }
 }
diff --git a/pumpkin-world/src/block/mod.rs b/pumpkin-world/src/block/mod.rs
index 75315f5e44..ce2ebb633b 100644
--- a/pumpkin-world/src/block/mod.rs
+++ b/pumpkin-world/src/block/mod.rs
@@ -9,7 +9,7 @@ use pumpkin_util::math::vector3::Vector3;
 use serde::Deserialize;
 pub use state::ChunkBlockState;
 
-#[derive(FromPrimitive, PartialEq, Clone, Copy, Debug)]
+#[derive(FromPrimitive, PartialEq, Clone, Copy, Debug, Hash, Eq)]
 pub enum BlockDirection {
     Down = 0,
     Up,
diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs
index 0aa7eee4da..b20eecb2b1 100644
--- a/pumpkin-world/src/level.rs
+++ b/pumpkin-world/src/level.rs
@@ -63,6 +63,7 @@ pub struct Level {
     // TODO: Make this a trait
     _locker: Arc<AnvilLevelLocker>,
     block_ticks: Arc<Mutex<Vec<ScheduledTick>>>,
+    fluid_ticks: Arc<Mutex<Vec<ScheduledTick>>>,
     /// Tracks tasks associated with this world instance
     tasks: TaskTracker,
     /// Notification that interrupts tasks for shutdown
@@ -143,6 +144,7 @@ impl Level {
             tasks: TaskTracker::new(),
             shutdown_notifier: Notify::new(),
             block_ticks: Arc::new(Mutex::new(Vec::new())),
+            fluid_ticks: Arc::new(Mutex::new(Vec::new())),
         }
     }
 
@@ -350,10 +352,12 @@ impl Level {
             return;
         }
         let mut block_ticks = self.block_ticks.lock().await;
+        let mut fluid_ticks = self.fluid_ticks.lock().await;
 
         for (coord, chunk) in &chunks_to_write {
             let mut chunk_data = chunk.write().await;
             chunk_data.block_ticks.clear();
+            chunk_data.fluid_ticks.clear();
             // Only keep ticks that are not saved in the chunk
             block_ticks.retain(|tick| {
                 let (chunk_coord, _relative_coord) =
@@ -365,8 +369,19 @@ impl Level {
                     true
                 }
             });
+            fluid_ticks.retain(|tick| {
+                let (chunk_coord, _relative_coord) =
+                    tick.block_pos.chunk_and_chunk_relative_position();
+                if chunk_coord == *coord {
+                    chunk_data.fluid_ticks.push(tick.clone());
+                    false
+                } else {
+                    true
+                }
+            });
         }
         drop(block_ticks);
+        drop(fluid_ticks);
 
         let chunk_saver = self.chunk_saver.clone();
         let level_folder = self.level_folder.clone();
@@ -449,6 +464,7 @@ impl Level {
         let load_channel = channel.clone();
         let loaded_chunks = self.loaded_chunks.clone();
         let level_block_ticks = self.block_ticks.clone();
+        let level_fluid_ticks = self.fluid_ticks.clone();
         let handle_load = async move {
             while let Some(data) = load_bridge_recv.recv().await {
                 let is_ok = match data {
@@ -461,6 +477,12 @@ impl Level {
                         level_block_ticks.extend(block_ticks);
                         drop(level_block_ticks);
 
+                        // Load the fluid ticks from the chunk
+                        let fluid_ticks = chunk.read().await.fluid_ticks.clone();
+                        let mut level_fluid_ticks = level_fluid_ticks.lock().await;
+                        level_fluid_ticks.extend(fluid_ticks);
+                        drop(level_fluid_ticks);
+
                         let value = loaded_chunks
                             .entry(position)
                             .or_insert(chunk)
@@ -571,6 +593,21 @@ impl Level {
         ticks
     }
 
+    pub async fn get_and_tick_fluid_ticks(&self) -> Vec<ScheduledTick> {
+        let mut fluid_ticks = self.fluid_ticks.lock().await;
+        let mut ticks = Vec::new();
+        fluid_ticks.retain_mut(|tick| {
+            tick.delay = tick.delay.saturating_sub(1);
+            if tick.delay == 0 {
+                ticks.push(tick.clone());
+                false
+            } else {
+                true
+            }
+        });
+        ticks
+    }
+
     pub async fn is_block_tick_scheduled(&self, block_pos: &BlockPos, block_id: u16) -> bool {
         let block_ticks = self.block_ticks.lock().await;
         block_ticks
@@ -578,6 +615,11 @@ impl Level {
             .any(|tick| tick.block_pos == *block_pos && tick.target_block_id == block_id)
     }
 
+    pub async fn is_fluid_tick_scheduled(&self, block_pos: &BlockPos) -> bool {
+        let fluid_ticks = self.fluid_ticks.lock().await;
+        fluid_ticks.iter().any(|tick| tick.block_pos == *block_pos)
+    }
+
     pub async fn schedule_block_tick(
         &self,
         block_id: u16,
@@ -593,4 +635,14 @@ impl Level {
             target_block_id: block_id,
         });
     }
+
+    pub async fn schedule_fluid_tick(&self, block_id: u16, block_pos: &BlockPos, delay: u16) {
+        let mut fluid_ticks = self.fluid_ticks.lock().await;
+        fluid_ticks.push(ScheduledTick {
+            block_pos: *block_pos,
+            delay,
+            priority: TickPriority::Normal,
+            target_block_id: block_id,
+        });
+    }
 }
diff --git a/pumpkin/src/block/blocks/fluids/flowing_fluid.rs b/pumpkin/src/block/blocks/fluids/flowing_fluid.rs
new file mode 100644
index 0000000000..30d84310a7
--- /dev/null
+++ b/pumpkin/src/block/blocks/fluids/flowing_fluid.rs
@@ -0,0 +1,439 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::{
+    block::Block,
+    fluid::{EnumVariants, Falling, Fluid, FluidProperties, Level},
+};
+use pumpkin_util::math::position::BlockPos;
+use pumpkin_world::block::BlockDirection;
+
+use crate::world::{BlockFlags, World};
+type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
+
+#[derive(Clone)]
+pub struct SpreadContext {
+    holes: HashMap<BlockPos, bool>,
+}
+
+impl SpreadContext {
+    pub fn new() -> Self {
+        Self {
+            holes: HashMap::new(),
+        }
+    }
+
+    pub async fn is_hole<T: FlowingFluid + ?Sized + std::marker::Sync>(
+        &mut self,
+        fluid: &T,
+        world: &Arc<World>,
+        fluid_type: &Fluid,
+        pos: &BlockPos,
+    ) -> bool {
+        if let Some(is_hole) = self.holes.get(pos) {
+            return *is_hole;
+        }
+
+        let below_pos = pos.down();
+        let is_hole = fluid
+            .is_water_hole(world, fluid_type, pos, &below_pos)
+            .await;
+
+        self.holes.insert(*pos, is_hole);
+        is_hole
+    }
+}
+
+#[async_trait]
+pub trait FlowingFluid {
+    async fn get_drop_off(&self) -> i32;
+
+    async fn get_source(&self, fluid: &Fluid, falling: bool) -> FlowingFluidProperties {
+        let mut source_props = FlowingFluidProperties::default(fluid);
+        source_props.level = Level::L8;
+        source_props.falling = if falling {
+            Falling::True
+        } else {
+            Falling::False
+        };
+        source_props
+    }
+
+    async fn get_flowing(
+        &self,
+        fluid: &Fluid,
+        level: Level,
+        falling: bool,
+    ) -> FlowingFluidProperties {
+        let mut flowing_props = FlowingFluidProperties::default(fluid);
+        flowing_props.level = level;
+        flowing_props.falling = if falling {
+            Falling::True
+        } else {
+            Falling::False
+        };
+        flowing_props
+    }
+
+    async fn get_slope_find_distance(&self) -> i32;
+
+    async fn can_convert_to_source(&self, world: &Arc<World>) -> bool;
+
+    fn is_same_fluid(&self, fluid: &Fluid, other_state_id: u16) -> bool {
+        if let Some(other_fluid) = Fluid::from_state_id(other_state_id) {
+            return fluid.id == other_fluid.id;
+        }
+        false
+    }
+
+    async fn spread_fluid(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        let Ok(block_state_id) = world.get_block_state_id(block_pos).await else {
+            return;
+        };
+        if let Some(new_fluid_state) = self.get_new_liquid(world, fluid, block_pos).await {
+            if new_fluid_state.to_state_id(fluid) != block_state_id {
+                world
+                    .set_block_state(
+                        block_pos,
+                        new_fluid_state.to_state_id(fluid),
+                        BlockFlags::NOTIFY_ALL,
+                    )
+                    .await;
+            }
+            self.spread(world, fluid, block_pos, &new_fluid_state).await;
+        } else {
+            world
+                .break_block(block_pos, None, BlockFlags::empty())
+                .await;
+            world
+                .set_block_state(
+                    block_pos,
+                    Block::AIR.default_state_id,
+                    BlockFlags::NOTIFY_ALL,
+                )
+                .await;
+        }
+    }
+
+    async fn spread(
+        &self,
+        world: &Arc<World>,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+        props: &FlowingFluidProperties,
+    ) {
+        let below_pos = block_pos.down();
+        let below_can_replace = !self.is_solid_or_source(world, &below_pos, 0, fluid).await;
+
+        if below_can_replace {
+            let mut new_props = FlowingFluidProperties::default(fluid);
+            new_props.level = Level::L8;
+            new_props.falling = Falling::True;
+
+            self.spread_to(world, fluid, &below_pos, new_props.to_state_id(fluid))
+                .await;
+        } else if props.level == Level::L8 && props.falling == Falling::False
+            || !self
+                .is_water_hole(world, fluid, block_pos, &below_pos)
+                .await
+        {
+            self.spread_to_sides(world, fluid, block_pos).await;
+        }
+    }
+
+    async fn get_new_liquid(
+        &self,
+        world: &Arc<World>,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+    ) -> Option<FlowingFluidProperties> {
+        let Ok(current_state_id) = world.get_block_state_id(block_pos).await else {
+            return None;
+        };
+
+        let current_props = FlowingFluidProperties::from_state_id(current_state_id, fluid);
+        let current_level = i32::from(current_props.level.to_index()) + 1;
+        if current_level == 8 && current_props.falling != Falling::True {
+            return Some(current_props);
+        }
+        let mut highest_level = 0;
+        let mut source_count = 0;
+
+        for direction in BlockDirection::horizontal() {
+            let neighbor_pos = block_pos.offset(direction.to_offset());
+            let Ok(neighbor_state_id) = world.get_block_state_id(&neighbor_pos).await else {
+                continue;
+            };
+
+            if !self.is_same_fluid(fluid, neighbor_state_id) {
+                continue;
+            }
+
+            let neighbor_props = FlowingFluidProperties::from_state_id(neighbor_state_id, fluid);
+            let neighbor_level = i32::from(neighbor_props.level.to_index()) + 1;
+
+            if neighbor_level == 8 && neighbor_props.falling != Falling::True {
+                source_count += 1;
+            }
+
+            highest_level = highest_level.max(neighbor_level);
+        }
+
+        if source_count >= 2 && self.can_convert_to_source(world).await {
+            let below_pos = block_pos.down();
+            let Ok(below_state_id) = world.get_block_state_id(&below_pos).await else {
+                return Some(current_props);
+            };
+            if self
+                .is_solid_or_source(world, &below_pos, below_state_id, fluid)
+                .await
+            {
+                return Some(self.get_source(fluid, false).await);
+            }
+        }
+
+        let above_pos = block_pos.up();
+        let Ok(above_state_id) = world.get_block_state_id(&above_pos).await else {
+            return Some(current_props);
+        };
+
+        if self.is_same_fluid(fluid, above_state_id) {
+            return Some(self.get_flowing(fluid, Level::L8, true).await);
+        }
+
+        let drop_off = self.get_drop_off().await;
+        let new_level = highest_level - drop_off;
+
+        if new_level <= 0 {
+            return None;
+        }
+        if new_level != current_level {
+            return Some(
+                self.get_flowing(fluid, Level::from_index(new_level as u16 - 1), false)
+                    .await,
+            );
+        }
+        Some(current_props)
+    }
+
+    async fn is_solid_or_source(
+        &self,
+        world: &Arc<World>,
+        block_pos: &BlockPos,
+        state_id: u16,
+        fluid: &Fluid,
+    ) -> bool {
+        let Ok(block) = world.get_block(block_pos).await else {
+            return false;
+        };
+
+        if block.id != 0 && !self.can_be_replaced(world, block_pos, block.id).await {
+            return true;
+        }
+
+        if self.is_same_fluid(fluid, state_id) {
+            let props = FlowingFluidProperties::from_state_id(state_id, fluid);
+            return props.level == Level::L8 && props.falling != Falling::True;
+        }
+
+        false
+    }
+
+    async fn spread_to_sides(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        let Ok(block_state_id) = world.get_block_state_id(block_pos).await else {
+            return;
+        };
+
+        let props = FlowingFluidProperties::from_state_id(block_state_id, fluid);
+        let level = i32::from(props.level.to_index()) + 1;
+
+        let effective_level = if props.falling == Falling::True {
+            7
+        } else {
+            level
+        };
+
+        let drop_off = self.get_drop_off().await;
+        let new_level = effective_level - drop_off;
+
+        if new_level <= 0 {
+            return;
+        }
+
+        let spread_dirs = self.get_spread(world, fluid, block_pos).await;
+
+        for (direction, _slope_dist) in spread_dirs {
+            let side_pos = block_pos.offset(direction.to_offset());
+
+            if self.can_replace_block(world, &side_pos, fluid).await {
+                let new_props = self
+                    .get_flowing(fluid, Level::from_index(new_level as u16 - 1), false)
+                    .await;
+                self.spread_to(world, fluid, &side_pos, new_props.to_state_id(fluid))
+                    .await;
+            }
+        }
+    }
+
+    async fn get_spread(
+        &self,
+        world: &Arc<World>,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+    ) -> HashMap<BlockDirection, i32> {
+        let mut min_dist = 1000;
+        let mut result = HashMap::new();
+        let mut ctx = None;
+        for direction in BlockDirection::horizontal() {
+            let side_pos = block_pos.offset(direction.to_offset());
+            let Ok(side_state_id) = world.get_block_state_id(&side_pos).await else {
+                continue;
+            };
+
+            if !self.can_pass_through(world, fluid, &side_pos).await
+                || self.is_same_fluid(fluid, side_state_id)
+            {
+                continue;
+            }
+
+            if ctx.is_none() {
+                ctx = Some(SpreadContext::new());
+            }
+
+            let ctx_ref = ctx.as_mut().unwrap();
+
+            let slope_dist = if ctx_ref.is_hole(self, world, fluid, &side_pos).await {
+                0
+            } else {
+                self.get_slope_distance(world, fluid, side_pos, 1, direction.opposite(), ctx_ref)
+                    .await
+            };
+
+            if slope_dist < min_dist {
+                min_dist = slope_dist;
+                result.clear();
+            }
+
+            if slope_dist <= min_dist {
+                result.insert(direction, slope_dist);
+            }
+        }
+        result
+    }
+
+    async fn get_slope_distance(
+        &self,
+        world: &Arc<World>,
+        fluid: &Fluid,
+        block_pos: BlockPos,
+        distance: i32,
+        exclude_dir: BlockDirection,
+        ctx: &mut SpreadContext,
+    ) -> i32 {
+        if distance > self.get_slope_find_distance().await {
+            return distance;
+        }
+
+        let mut min_dist = 1000;
+
+        for direction in BlockDirection::horizontal() {
+            if direction == exclude_dir {
+                continue;
+            }
+
+            let next_pos = block_pos.offset(direction.to_offset());
+
+            if !self.can_pass_through(world, fluid, &next_pos).await {
+                continue;
+            }
+
+            if ctx.is_hole(self, world, fluid, &next_pos).await {
+                return distance;
+            }
+
+            let next_dist = self
+                .get_slope_distance(
+                    world,
+                    fluid,
+                    next_pos,
+                    distance + 1,
+                    direction.opposite(),
+                    ctx,
+                )
+                .await;
+
+            min_dist = min_dist.min(next_dist);
+        }
+        min_dist
+    }
+
+    async fn spread_to(&self, world: &Arc<World>, _fluid: &Fluid, pos: &BlockPos, state_id: u16) {
+        //TODO Implement lava water mix
+
+        world
+            .set_block_state(pos, state_id, BlockFlags::NOTIFY_ALL)
+            .await;
+    }
+
+    async fn can_pass_through(&self, world: &Arc<World>, fluid: &Fluid, pos: &BlockPos) -> bool {
+        let Ok(state_id) = world.get_block_state_id(pos).await else {
+            return false;
+        };
+
+        if self.is_same_fluid(fluid, state_id) {
+            return true;
+        }
+
+        self.can_replace_block(world, pos, fluid).await
+    }
+
+    async fn can_replace_block(&self, world: &Arc<World>, pos: &BlockPos, _fluid: &Fluid) -> bool {
+        let Ok(block) = world.get_block(pos).await else {
+            return false;
+        };
+
+        if self.can_be_replaced(world, pos, block.id).await {
+            return true;
+        }
+
+        false
+    }
+
+    async fn can_be_replaced(&self, world: &Arc<World>, pos: &BlockPos, block_id: u16) -> bool {
+        let Ok(block_state_id) = world.get_block_state_id(pos).await else {
+            return false;
+        };
+
+        if let Some(fluid) = Fluid::from_state_id(block_state_id) {
+            if fluid.is_source(block_state_id) && fluid.is_falling(block_state_id) {
+                return true;
+            }
+        }
+
+        //TODO Add check for blocks that aren't solid
+        matches!(block_id, 0)
+    }
+
+    async fn is_water_hole(
+        &self,
+        world: &Arc<World>,
+        fluid: &Fluid,
+        _pos: &BlockPos,
+        below_pos: &BlockPos,
+    ) -> bool {
+        let Ok(below_state_id) = world.get_block_state_id(below_pos).await else {
+            return false;
+        };
+
+        if self.is_same_fluid(fluid, below_state_id) {
+            return true;
+        }
+
+        if self.can_replace_block(world, below_pos, fluid).await {
+            return true;
+        }
+
+        false
+    }
+}
diff --git a/pumpkin/src/block/blocks/fluids/lava.rs b/pumpkin/src/block/blocks/fluids/lava.rs
new file mode 100644
index 0000000000..a9fb17c627
--- /dev/null
+++ b/pumpkin/src/block/blocks/fluids/lava.rs
@@ -0,0 +1,64 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::fluid::Fluid;
+use pumpkin_macros::pumpkin_block;
+use pumpkin_util::math::position::BlockPos;
+
+use crate::{block::pumpkin_fluid::PumpkinFluid, world::World};
+
+use super::flowing_fluid::FlowingFluid;
+
+#[pumpkin_block("minecraft:flowing_lava")]
+pub struct FlowingLava;
+
+const LAVA_FLOW_SPEED: u16 = 30;
+
+#[async_trait]
+impl PumpkinFluid for FlowingLava {
+    async fn placed(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        _state_id: u16,
+        block_pos: &BlockPos,
+        _old_state_id: u16,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
+            .await;
+    }
+
+    async fn on_scheduled_tick(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        self.spread_fluid(world, fluid, block_pos).await;
+    }
+
+    async fn on_neighbor_update(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
+            .await;
+    }
+}
+
+#[async_trait]
+impl FlowingFluid for FlowingLava {
+    async fn get_drop_off(&self) -> i32 {
+        2
+    }
+
+    async fn get_slope_find_distance(&self) -> i32 {
+        2
+    }
+
+    async fn can_convert_to_source(&self, _world: &Arc<World>) -> bool {
+        //TODO add game rule check for lava conversion
+        false
+    }
+}
diff --git a/pumpkin/src/block/blocks/fluids/mod.rs b/pumpkin/src/block/blocks/fluids/mod.rs
new file mode 100644
index 0000000000..5fab3e233b
--- /dev/null
+++ b/pumpkin/src/block/blocks/fluids/mod.rs
@@ -0,0 +1,3 @@
+pub(crate) mod flowing_fluid;
+pub(crate) mod lava;
+pub(crate) mod water;
diff --git a/pumpkin/src/block/blocks/fluids/water.rs b/pumpkin/src/block/blocks/fluids/water.rs
new file mode 100644
index 0000000000..22455af733
--- /dev/null
+++ b/pumpkin/src/block/blocks/fluids/water.rs
@@ -0,0 +1,64 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::fluid::Fluid;
+use pumpkin_macros::pumpkin_block;
+use pumpkin_util::math::position::BlockPos;
+
+use crate::{block::pumpkin_fluid::PumpkinFluid, world::World};
+
+use super::flowing_fluid::FlowingFluid;
+
+#[pumpkin_block("minecraft:flowing_water")]
+pub struct FlowingWater;
+
+const WATER_FLOW_SPEED: u16 = 5;
+
+#[async_trait]
+impl PumpkinFluid for FlowingWater {
+    async fn placed(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        _state_id: u16,
+        block_pos: &BlockPos,
+        _old_state_id: u16,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, WATER_FLOW_SPEED)
+            .await;
+    }
+
+    async fn on_scheduled_tick(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        self.spread_fluid(world, fluid, block_pos).await;
+    }
+
+    async fn on_neighbor_update(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, WATER_FLOW_SPEED)
+            .await;
+    }
+}
+
+#[async_trait]
+impl FlowingFluid for FlowingWater {
+    async fn get_drop_off(&self) -> i32 {
+        1
+    }
+
+    async fn get_slope_find_distance(&self) -> i32 {
+        4
+    }
+
+    async fn can_convert_to_source(&self, _world: &Arc<World>) -> bool {
+        //TODO add game rule check for water conversion
+        true
+    }
+}
diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs
index 9719f8d771..237a377942 100644
--- a/pumpkin/src/block/blocks/mod.rs
+++ b/pumpkin/src/block/blocks/mod.rs
@@ -13,6 +13,7 @@ pub(crate) mod doors;
 pub(crate) mod farmland;
 pub(crate) mod fence_gates;
 pub(crate) mod fences;
+pub(crate) mod fluids;
 pub(crate) mod furnace;
 pub(crate) mod jukebox;
 pub(crate) mod logs;
diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs
index de3988a66e..b75af18f64 100644
--- a/pumpkin/src/block/mod.rs
+++ b/pumpkin/src/block/mod.rs
@@ -4,6 +4,7 @@ use blocks::doors::register_door_blocks;
 use blocks::farmland::FarmLandBlock;
 use blocks::fence_gates::register_fence_gate_blocks;
 use blocks::fences::register_fence_blocks;
+use blocks::fluids::{lava::FlowingLava, water::FlowingWater};
 use blocks::logs::register_log_blocks;
 use blocks::redstone::buttons::register_button_blocks;
 use blocks::redstone::observer::ObserverBlock;
@@ -39,6 +40,7 @@ use std::sync::Arc;
 
 mod blocks;
 pub mod pumpkin_block;
+pub mod pumpkin_fluid;
 pub mod registry;
 
 #[must_use]
@@ -62,6 +64,8 @@ pub fn default_registry() -> Arc<BlockRegistry> {
     manager.register(ObserverBlock);
     manager.register(PistonBlock);
     manager.register(TargetBlock);
+    manager.register_fluid(FlowingWater);
+    manager.register_fluid(FlowingLava);
 
     register_door_blocks(&mut manager);
     register_fence_blocks(&mut manager);
diff --git a/pumpkin/src/block/pumpkin_fluid.rs b/pumpkin/src/block/pumpkin_fluid.rs
new file mode 100644
index 0000000000..c0232a5b5b
--- /dev/null
+++ b/pumpkin/src/block/pumpkin_fluid.rs
@@ -0,0 +1,84 @@
+use std::sync::Arc;
+
+use crate::entity::player::Player;
+use async_trait::async_trait;
+use pumpkin_data::{fluid::Fluid, item::Item};
+use pumpkin_protocol::server::play::SUseItemOn;
+use pumpkin_util::math::position::BlockPos;
+use pumpkin_world::block::BlockDirection;
+
+use crate::{server::Server, world::World};
+
+use super::registry::BlockActionResult;
+
+#[async_trait]
+pub trait PumpkinFluid: Send + Sync {
+    async fn normal_use(
+        &self,
+        _fluid: &Fluid,
+        _player: &Player,
+        _location: BlockPos,
+        _server: &Server,
+        _world: &World,
+    ) {
+    }
+    async fn use_with_item(
+        &self,
+        _fluid: &Fluid,
+        _player: &Player,
+        _location: BlockPos,
+        _item: &Item,
+        _server: &Server,
+        _world: &World,
+    ) -> BlockActionResult {
+        BlockActionResult::Continue
+    }
+
+    async fn placed(
+        &self,
+        _world: &World,
+        _fluid: &Fluid,
+        _state_id: u16,
+        _block_pos: &BlockPos,
+        _old_state_id: u16,
+        _notify: bool,
+    ) {
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn on_place(
+        &self,
+        _server: &Server,
+        _world: &World,
+        fluid: &Fluid,
+        _face: &BlockDirection,
+        _block_pos: &BlockPos,
+        _use_item_on: &SUseItemOn,
+        _other: bool,
+    ) -> u16 {
+        fluid.default_state_index
+    }
+
+    async fn get_state_for_neighbour_update(
+        &self,
+        _world: &World,
+        _fluid: &Fluid,
+        _block_pos: &BlockPos,
+        _notify: bool,
+    ) -> u16 {
+        0
+    }
+
+    async fn on_neighbor_update(
+        &self,
+        _world: &World,
+        _fluid: &Fluid,
+        _block_pos: &BlockPos,
+        _notify: bool,
+    ) {
+    }
+
+    async fn on_scheduled_tick(&self, _world: &Arc<World>, _fluid: &Fluid, _block_pos: &BlockPos) {}
+
+    async fn create_legacy_block(&self, _world: &World, _block_pos: &BlockPos) {}
+}
diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs
index 92ffd57bf2..6485063348 100644
--- a/pumpkin/src/block/registry.rs
+++ b/pumpkin/src/block/registry.rs
@@ -3,6 +3,7 @@ use crate::entity::player::Player;
 use crate::server::Server;
 use crate::world::{BlockFlags, World};
 use pumpkin_data::block::{Block, BlockState, HorizontalFacing};
+use pumpkin_data::fluid::Fluid;
 use pumpkin_data::item::Item;
 use pumpkin_inventory::OpenContainer;
 use pumpkin_protocol::server::play::SUseItemOn;
@@ -11,6 +12,8 @@ use pumpkin_world::block::BlockDirection;
 use std::collections::HashMap;
 use std::sync::Arc;
 
+use super::pumpkin_fluid::PumpkinFluid;
+
 pub enum BlockActionResult {
     /// Allow other actions to be executed
     Continue,
@@ -21,6 +24,7 @@ pub enum BlockActionResult {
 #[derive(Default)]
 pub struct BlockRegistry {
     blocks: HashMap<String, Arc<dyn PumpkinBlock>>,
+    fluids: HashMap<String, Arc<dyn PumpkinFluid>>,
 }
 
 impl BlockRegistry {
@@ -28,6 +32,10 @@ impl BlockRegistry {
         self.blocks.insert(block.name(), Arc::new(block));
     }
 
+    pub fn register_fluid<T: PumpkinFluid + BlockMetadata + 'static>(&mut self, fluid: T) {
+        self.fluids.insert(fluid.name(), Arc::new(fluid));
+    }
+
     pub async fn on_use(
         &self,
         block: &Block,
@@ -69,6 +77,24 @@ impl BlockRegistry {
         BlockActionResult::Continue
     }
 
+    pub async fn use_with_item_fluid(
+        &self,
+        fluid: &Fluid,
+        player: &Player,
+        location: BlockPos,
+        item: &Item,
+        server: &Server,
+        world: &Arc<World>,
+    ) -> BlockActionResult {
+        let pumpkin_fluid = self.get_pumpkin_fluid(fluid);
+        if let Some(pumpkin_fluid) = pumpkin_fluid {
+            return pumpkin_fluid
+                .use_with_item(fluid, player, location, item, server, world)
+                .await;
+        }
+        BlockActionResult::Continue
+    }
+
     #[allow(clippy::too_many_arguments)]
     pub async fn on_place(
         &self,
@@ -124,6 +150,23 @@ impl BlockRegistry {
         }
     }
 
+    pub async fn on_placed_fluid(
+        &self,
+        world: &Arc<World>,
+        fluid: &Fluid,
+        state_id: u16,
+        block_pos: &BlockPos,
+        old_state_id: u16,
+        notify: bool,
+    ) {
+        let pumpkin_fluid = self.get_pumpkin_fluid(fluid);
+        if let Some(pumpkin_fluid) = pumpkin_fluid {
+            pumpkin_fluid
+                .placed(world, fluid, state_id, block_pos, old_state_id, notify)
+                .await;
+        }
+    }
+
     pub async fn broken(
         &self,
         world: Arc<World>,
@@ -288,6 +331,12 @@ impl BlockRegistry {
             .get(format!("minecraft:{}", block.name).as_str())
     }
 
+    #[must_use]
+    pub fn get_pumpkin_fluid(&self, fluid: &Fluid) -> Option<&Arc<dyn PumpkinFluid>> {
+        self.fluids
+            .get(format!("{}:{}", "minecraft", fluid.name).as_str())
+    }
+
     pub async fn emits_redstone_power(
         &self,
         block: &Block,
diff --git a/pumpkin/src/command/commands/setblock.rs b/pumpkin/src/command/commands/setblock.rs
index e1f182c5a7..54f1ae64a0 100644
--- a/pumpkin/src/command/commands/setblock.rs
+++ b/pumpkin/src/command/commands/setblock.rs
@@ -55,20 +55,32 @@ impl CommandExecutor for Executor {
                     .break_block(&pos, None, BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE)
                     .await;
                 world
-                    .set_block_state(&pos, block_state_id, BlockFlags::FORCE_STATE)
+                    .set_block_state(
+                        &pos,
+                        block_state_id,
+                        BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS,
+                    )
                     .await;
                 true
             }
             Mode::Replace => {
                 world
-                    .set_block_state(&pos, block_state_id, BlockFlags::FORCE_STATE)
+                    .set_block_state(
+                        &pos,
+                        block_state_id,
+                        BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS,
+                    )
                     .await;
                 true
             }
             Mode::Keep => match world.get_block_state(&pos).await {
                 Ok(old_state) if old_state.air => {
                     world
-                        .set_block_state(&pos, block_state_id, BlockFlags::FORCE_STATE)
+                        .set_block_state(
+                            &pos,
+                            block_state_id,
+                            BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS,
+                        )
                         .await;
                     true
                 }
diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs
index 589d553497..46bea8be60 100644
--- a/pumpkin/src/world/mod.rs
+++ b/pumpkin/src/world/mod.rs
@@ -28,6 +28,7 @@ use pumpkin_config::BasicConfiguration;
 use pumpkin_data::{
     block::Block,
     entity::{EntityStatus, EntityType},
+    fluid::Fluid,
     particle::Particle,
     sound::{Sound, SoundCategory},
     world::{RAW, WorldEvent},
@@ -509,6 +510,7 @@ impl World {
 
     pub async fn tick_scheduled_block_ticks(self: &Arc<Self>) {
         let blocks_to_tick = self.level.get_and_tick_block_ticks().await;
+        let fluids_to_tick = self.level.get_and_tick_fluid_ticks().await;
 
         for scheduled_tick in blocks_to_tick {
             let block = self.get_block(&scheduled_tick.block_pos).await.unwrap();
@@ -521,6 +523,20 @@ impl World {
                     .await;
             }
         }
+
+        for scheduled_tick in fluids_to_tick {
+            let Ok(fluid) = self.get_fluid(&scheduled_tick.block_pos).await else {
+                continue;
+            };
+            if scheduled_tick.target_block_id != fluid.id {
+                continue;
+            }
+            if let Some(pumpkin_fluid) = self.block_registry.get_pumpkin_fluid(&fluid) {
+                pumpkin_fluid
+                    .on_scheduled_tick(self, &fluid, &scheduled_tick.block_pos)
+                    .await;
+            }
+        }
     }
 
     /// Gets the y position of the first non air block from the top down
@@ -1313,6 +1329,7 @@ impl World {
     }
 
     /// Sets a block
+    #[allow(clippy::too_many_lines)]
     pub async fn set_block_state(
         self: &Arc<Self>,
         position: &BlockPos,
@@ -1365,6 +1382,7 @@ impl World {
 
         let block_state = self.get_block_state(position).await.unwrap();
         let new_block = Block::from_state_id(block_state_id).unwrap();
+        let new_fluid = self.get_fluid(position).await.unwrap_or(Fluid::EMPTY);
 
         // WorldChunk.java line 318
         if !flags.contains(BlockFlags::SKIP_BLOCK_ADDED_CALLBACK) && new_block != old_block {
@@ -1378,6 +1396,16 @@ impl World {
                     block_moved,
                 )
                 .await;
+            self.block_registry
+                .on_placed_fluid(
+                    self,
+                    &new_fluid,
+                    block_state_id,
+                    position,
+                    replaced_block_state_id,
+                    block_moved,
+                )
+                .await;
         }
 
         // Ig they do this cause it could be modified in chunkPos.setBlockState?
@@ -1439,6 +1467,12 @@ impl World {
             .await;
     }
 
+    pub async fn schedule_fluid_tick(&self, block_id: u16, block_pos: BlockPos, delay: u16) {
+        self.level
+            .schedule_fluid_tick(block_id, &block_pos, delay)
+            .await;
+    }
+
     pub async fn is_block_tick_scheduled(&self, block_pos: &BlockPos, block: &Block) -> bool {
         self.level
             .is_block_tick_scheduled(block_pos, block.id)
@@ -1551,6 +1585,14 @@ impl World {
         get_block_by_state_id(id).ok_or(GetBlockError::InvalidBlockId)
     }
 
+    pub async fn get_fluid(
+        &self,
+        position: &BlockPos,
+    ) -> Result<pumpkin_data::fluid::Fluid, GetBlockError> {
+        let id = self.get_block_state_id(position).await?;
+        Fluid::from_state_id(id).ok_or(GetBlockError::InvalidBlockId)
+    }
+
     /// Gets the `BlockState` from the block registry. Returns `None` if the block state was not found.
     pub async fn get_block_state(
         &self,
@@ -1589,6 +1631,7 @@ impl World {
             }
             let neighbor_pos = block_pos.offset(direction.to_offset());
             let neighbor_block = self.get_block(&neighbor_pos).await;
+            let neighbor_fluid = self.get_fluid(&neighbor_pos).await;
             if let Ok(neighbor_block) = neighbor_block {
                 if let Some(neighbor_pumpkin_block) =
                     self.block_registry.get_pumpkin_block(&neighbor_block)
@@ -1604,6 +1647,15 @@ impl World {
                         .await;
                 }
             }
+            if let Ok(neighbor_fluid) = neighbor_fluid {
+                if let Some(neighbor_pumpkin_fluid) =
+                    self.block_registry.get_pumpkin_fluid(&neighbor_fluid)
+                {
+                    neighbor_pumpkin_fluid
+                        .on_neighbor_update(self, &neighbor_fluid, &neighbor_pos, false)
+                        .await;
+                }
+            }
         }
     }
 


Reviews:
user: login: 4lve
body: Looks good, just have some questions here
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T17:41:53Z
user: login: teknostom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T20:05:30Z
user: login: teknostom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T20:05:31Z
user: login: teknostom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T20:05:32Z

Review comments:
diff_hunk: @@ -0,0 +1,466 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::{
+    block::Block,
+    fluid::{Falling, Fluid, FluidProperties, Level},
+};
+use pumpkin_util::math::position::BlockPos;
+use pumpkin_world::block::BlockDirection;
+
+use crate::world::{BlockFlags, World};
+type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
+
+fn level_to_int(level: Level) -> i32 {
path: pumpkin/src/block/blocks/fluids/flowing_fluid.rs
user: login: 4lve
body: You should be able to use Level.from_index and to_index
created_at: 2025-04-03T17:33:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,466 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::{
+    block::Block,
+    fluid::{Falling, Fluid, FluidProperties, Level},
+};
+use pumpkin_util::math::position::BlockPos;
+use pumpkin_world::block::BlockDirection;
+
+use crate::world::{BlockFlags, World};
+type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
+
+fn level_to_int(level: Level) -> i32 {
+    match level {
+        Level::L1 => 1,
+        Level::L2 => 2,
+        Level::L3 => 3,
+        Level::L4 => 4,
+        Level::L5 => 5,
+        Level::L6 => 6,
+        Level::L7 => 7,
+        Level::L8 => 8,
+    }
+}
+
+fn int_to_level(level: i32) -> Level {
+    match level {
+        2 => Level::L2,
+        3 => Level::L3,
+        4 => Level::L4,
+        5 => Level::L5,
+        6 => Level::L6,
+        7 => Level::L7,
+        8 => Level::L8,
+        _ => Level::L1,
+    }
+}
+
+#[derive(Clone)]
+pub struct SpreadContext {
+    holes: HashMap<BlockPos, bool>,
+}
+
+impl SpreadContext {
+    pub fn new() -> Self {
+        Self {
+            holes: HashMap::new(),
+        }
+    }
+
+    pub async fn is_hole<T: FlowingFluid + ?Sized + std::marker::Sync>(
+        &mut self,
+        fluid: &T,
+        world: &Arc<World>,
+        fluid_type: &Fluid,
+        pos: &BlockPos,
+    ) -> bool {
+        if let Some(is_hole) = self.holes.get(pos) {
+            return *is_hole;
+        }
+
+        let below_pos = pos.down();
+        let is_hole = fluid
+            .is_water_hole(world, fluid_type, pos, &below_pos)
+            .await;
+
+        self.holes.insert(*pos, is_hole);
+        is_hole
+    }
+}
+
+#[async_trait]
+pub trait FlowingFluid {
+    async fn get_drop_off(&self) -> i32;
+
+    async fn get_source(&self, fluid: &Fluid, falling: bool) -> FlowingFluidProperties {
+        let mut source_props = FlowingFluidProperties::default(fluid);
+        source_props.level = Level::L8;
+        source_props.falling = if falling {
+            Falling::True
+        } else {
+            Falling::False
+        };
+        source_props
+    }
+
+    async fn get_flowing(
+        &self,
+        fluid: &Fluid,
+        level: Level,
+        falling: bool,
+    ) -> FlowingFluidProperties {
+        let mut flowing_props = FlowingFluidProperties::default(fluid);
+        flowing_props.level = level;
+        flowing_props.falling = if falling {
+            Falling::True
+        } else {
+            Falling::False
+        };
+        flowing_props
+    }
+
+    async fn get_slope_find_distance(&self) -> i32;
+
+    async fn can_convert_to_source(&self, world: &Arc<World>) -> bool;
+
+    fn is_same_fluid(&self, fluid: &Fluid, other_state_id: u16) -> bool {
+        if let Some(other_fluid) = Fluid::from_state_id(other_state_id) {
+            return fluid.id == other_fluid.id;
+        }
+        false
+    }
+
+    async fn spread_fluid(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        let Ok(block_state_id) = world.get_block_state_id(block_pos).await else {
+            return;
+        };
+        if let Some(new_fluid_state) = self.get_new_liquid(world, fluid, block_pos).await {
+            if new_fluid_state.to_state_id(fluid) != block_state_id {
+                world
+                    .set_block_state(
+                        block_pos,
+                        new_fluid_state.to_state_id(fluid),
+                        BlockFlags::NOTIFY_ALL,
+                    )
+                    .await;
+            }
+            self.spread(world, fluid, block_pos, &new_fluid_state).await;
+        } else {
+            world
path: pumpkin/src/block/blocks/fluids/flowing_fluid.rs
user: login: 4lve
body: maybe world.break_block instead so it drops items?
created_at: 2025-04-03T17:35:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::fluid::Fluid;
+use pumpkin_macros::pumpkin_block;
+use pumpkin_util::math::position::BlockPos;
+
+use crate::{block::pumpkin_fluid::PumpkinFluid, world::World};
+
+use super::flowing_fluid::FlowingFluid;
+
+#[pumpkin_block("minecraft:flowing_lava")]
+pub struct FlowingLava;
+
+const LAVA_FLOW_SPEED: u16 = 30;
+
+#[async_trait]
+impl PumpkinFluid for FlowingLava {
+    async fn placed(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        _state_id: u16,
+        block_pos: &BlockPos,
+        _old_state_id: u16,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
+            .await;
+    }
+
+    async fn on_scheduled_tick(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        self.spread_fluid(world, fluid, block_pos).await;
+    }
+
+    async fn on_neighbor_update(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
+            .await;
+    }
+}
+
+#[async_trait]
+impl FlowingFluid for FlowingLava {
+    async fn get_drop_off(&self) -> i32 {
+        2
+    }
+
+    async fn get_slope_find_distance(&self) -> i32 {
+        2
+    }
+
+    async fn can_convert_to_source(&self, _world: &Arc<World>) -> bool {
path: pumpkin/src/block/blocks/fluids/lava.rs
user: login: 4lve
body: Lava can't convert to source right?
created_at: 2025-04-03T17:39:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,64 @@
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::fluid::Fluid;
+use pumpkin_macros::pumpkin_block;
+use pumpkin_util::math::position::BlockPos;
+
+use crate::{block::pumpkin_fluid::PumpkinFluid, world::World};
+
+use super::flowing_fluid::FlowingFluid;
+
+#[pumpkin_block("minecraft:flowing_lava")]
+pub struct FlowingLava;
+
+const LAVA_FLOW_SPEED: u16 = 30;
+
+#[async_trait]
+impl PumpkinFluid for FlowingLava {
+    async fn placed(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        _state_id: u16,
+        block_pos: &BlockPos,
+        _old_state_id: u16,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
+            .await;
+    }
+
+    async fn on_scheduled_tick(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        self.spread_fluid(world, fluid, block_pos).await;
+    }
+
+    async fn on_neighbor_update(
+        &self,
+        world: &World,
+        fluid: &Fluid,
+        block_pos: &BlockPos,
+        _notify: bool,
+    ) {
+        world
+            .schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
+            .await;
+    }
+}
+
+#[async_trait]
+impl FlowingFluid for FlowingLava {
+    async fn get_drop_off(&self) -> i32 {
+        2
+    }
+
+    async fn get_slope_find_distance(&self) -> i32 {
+        2
+    }
+
+    async fn can_convert_to_source(&self, _world: &Arc<World>) -> bool {
path: pumpkin/src/block/blocks/fluids/lava.rs
user: login: teknostom
body: Yep, true
created_at: 2025-04-03T20:05:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,466 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::{
+    block::Block,
+    fluid::{Falling, Fluid, FluidProperties, Level},
+};
+use pumpkin_util::math::position::BlockPos;
+use pumpkin_world::block::BlockDirection;
+
+use crate::world::{BlockFlags, World};
+type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
+
+fn level_to_int(level: Level) -> i32 {
+    match level {
+        Level::L1 => 1,
+        Level::L2 => 2,
+        Level::L3 => 3,
+        Level::L4 => 4,
+        Level::L5 => 5,
+        Level::L6 => 6,
+        Level::L7 => 7,
+        Level::L8 => 8,
+    }
+}
+
+fn int_to_level(level: i32) -> Level {
+    match level {
+        2 => Level::L2,
+        3 => Level::L3,
+        4 => Level::L4,
+        5 => Level::L5,
+        6 => Level::L6,
+        7 => Level::L7,
+        8 => Level::L8,
+        _ => Level::L1,
+    }
+}
+
+#[derive(Clone)]
+pub struct SpreadContext {
+    holes: HashMap<BlockPos, bool>,
+}
+
+impl SpreadContext {
+    pub fn new() -> Self {
+        Self {
+            holes: HashMap::new(),
+        }
+    }
+
+    pub async fn is_hole<T: FlowingFluid + ?Sized + std::marker::Sync>(
+        &mut self,
+        fluid: &T,
+        world: &Arc<World>,
+        fluid_type: &Fluid,
+        pos: &BlockPos,
+    ) -> bool {
+        if let Some(is_hole) = self.holes.get(pos) {
+            return *is_hole;
+        }
+
+        let below_pos = pos.down();
+        let is_hole = fluid
+            .is_water_hole(world, fluid_type, pos, &below_pos)
+            .await;
+
+        self.holes.insert(*pos, is_hole);
+        is_hole
+    }
+}
+
+#[async_trait]
+pub trait FlowingFluid {
+    async fn get_drop_off(&self) -> i32;
+
+    async fn get_source(&self, fluid: &Fluid, falling: bool) -> FlowingFluidProperties {
+        let mut source_props = FlowingFluidProperties::default(fluid);
+        source_props.level = Level::L8;
+        source_props.falling = if falling {
+            Falling::True
+        } else {
+            Falling::False
+        };
+        source_props
+    }
+
+    async fn get_flowing(
+        &self,
+        fluid: &Fluid,
+        level: Level,
+        falling: bool,
+    ) -> FlowingFluidProperties {
+        let mut flowing_props = FlowingFluidProperties::default(fluid);
+        flowing_props.level = level;
+        flowing_props.falling = if falling {
+            Falling::True
+        } else {
+            Falling::False
+        };
+        flowing_props
+    }
+
+    async fn get_slope_find_distance(&self) -> i32;
+
+    async fn can_convert_to_source(&self, world: &Arc<World>) -> bool;
+
+    fn is_same_fluid(&self, fluid: &Fluid, other_state_id: u16) -> bool {
+        if let Some(other_fluid) = Fluid::from_state_id(other_state_id) {
+            return fluid.id == other_fluid.id;
+        }
+        false
+    }
+
+    async fn spread_fluid(&self, world: &Arc<World>, fluid: &Fluid, block_pos: &BlockPos) {
+        let Ok(block_state_id) = world.get_block_state_id(block_pos).await else {
+            return;
+        };
+        if let Some(new_fluid_state) = self.get_new_liquid(world, fluid, block_pos).await {
+            if new_fluid_state.to_state_id(fluid) != block_state_id {
+                world
+                    .set_block_state(
+                        block_pos,
+                        new_fluid_state.to_state_id(fluid),
+                        BlockFlags::NOTIFY_ALL,
+                    )
+                    .await;
+            }
+            self.spread(world, fluid, block_pos, &new_fluid_state).await;
+        } else {
+            world
path: pumpkin/src/block/blocks/fluids/flowing_fluid.rs
user: login: teknostom
body: Done
created_at: 2025-04-03T20:05:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,466 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use pumpkin_data::{
+    block::Block,
+    fluid::{Falling, Fluid, FluidProperties, Level},
+};
+use pumpkin_util::math::position::BlockPos;
+use pumpkin_world::block::BlockDirection;
+
+use crate::world::{BlockFlags, World};
+type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
+
+fn level_to_int(level: Level) -> i32 {
path: pumpkin/src/block/blocks/fluids/flowing_fluid.rs
user: login: teknostom
body: Done
created_at: 2025-04-03T20:05:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: Q2297045667
created_at: 2025-03-24T15:18:03Z
body: How does the performance of this compare to vanilla?
author_association: COLLABORATOR
user: login: teknostom
created_at: 2025-03-24T15:20:46Z
body: > How does the performance of this compare to vanilla?

It is not fully implemented yet, so no water flow algorithm has been implemented yet. But I have done a course in Software Optimization, so if I find space for improvement, I will make it as fast as I can.
author_association: CONTRIBUTOR
user: login: Snowiiii
created_at: 2025-03-28T07:10:12Z
body: @teknostom any progress on that ?. Would be super nice if this gets done because fluidstates are used in many places 
author_association: MEMBER
user: login: teknostom
created_at: 2025-03-28T07:13:30Z
body: Yeah, i have had some trouble with understanding the java code, I have gotten most things to work, but the "flow into holes" mechanic is definitely the hardest. Will try to finish this up this weekend though.
author_association: CONTRIBUTOR
user: login: Snowiiii
created_at: 2025-03-31T07:28:24Z
body: please move the fluid folder to blocks
author_association: MEMBER
user: login: Snowiiii
created_at: 2025-03-31T07:29:16Z
body: Also why you made a new trait and "register system" for fluids, i think they are the same as blocks
author_association: MEMBER
user: login: teknostom
created_at: 2025-04-01T16:13:28Z
body: @Snowiiii This PR should be done now. It includes both lava and water. Currently, it only works with setblock, but I could maybe work on buckets as a next PR.

It also works with neighbor updates now, so behaviour should be 100% match to vanilla.
author_association: CONTRIBUTOR
user: login: Snowiiii
created_at: 2025-04-01T16:45:11Z
body: Hey @teknostom, can you may make the width match Vanilla?
![image](https://github.com/user-attachments/assets/99624043-6832-494e-900c-7e052fff7a7c)
![image](https://github.com/user-attachments/assets/aa373729-9d74-436d-8f69-a8a744514881)

author_association: MEMBER
user: login: teknostom
created_at: 2025-04-01T16:46:26Z
body: Oh, weird. Will look into that.
author_association: CONTRIBUTOR
user: login: Snowiiii
created_at: 2025-04-01T16:48:21Z
body: Also could you please add support for water and lava which is located in the World, would be nice to have that in this PR as well
![image](https://github.com/user-attachments/assets/c98f8748-9596-495e-ae39-bb9858a499a9)
 
author_association: MEMBER
user: login: teknostom
created_at: 2025-04-03T15:22:36Z
body: The CI is failing due to the new rust release, but the two bugs are fixed. Water and lava should now flow from generated blocks.

One problem I have noticed is with the set_block_state. It does not always update the blocks for the client. Might be some async stuff there causing updates to be lost. But if you hold right click while changing blocks around water, it should work like vanilla.
author_association: CONTRIBUTOR
user: login: Snowiiii
created_at: 2025-04-03T17:12:49Z
body: Hey, Can you please merge master again
author_association: MEMBER
user: login: Snowiiii
created_at: 2025-04-05T09:54:36Z
body: Awesome work. Thank you @teknostom! :100: 
author_association: MEMBER