Repository: neoforged/NeoForge (Java)

Issue:
Hook for `BubbleColumnBlock#getColumnState`
The problem: The method `BubbleColumnBlock#getColumnState(BlockState)` is hard coded to only support magma blocks and soul sand. It is not possible to create modded blocks that provide bubble columns with the current API.

The failed solution: In my testing I found that I could call `BubbleColumnBlock#updateColumn(LevelAccessor, BlockPos, BlockState, BlockState)` with a spoofed block state to place bubble columns, but the bubble column verifies itself later in its tick method, so we need a hook.

The proposed solution: add a method to `IForgeBlockState` and `IForgeBlock` to allow modded blocks to provide a bubble column block state. 

Option 1. The new method replicates the logic found in `BubbleColumnBlock#getColumnState(BlockState)` . The original method has a hook injected at the start which redirects to the block state hook.

Option 2. The new method has a return type of `BlockState` and returns null by default. The original method has a call to the new method, and if the result is not null, it immediately returns the result. If the result is null, the rest of the method executes as normal.

Option 3. The new method has a return type of `Boolean` and returns null by default. The original method has a call to the new method, and if the result is not null, it returns the bubble column block state with the `DRAG_DOWN` property set to the specified value. This ensures that the method is only used for bubble columns and not for anything else. If the result is null, the rest of the method executes as normal.

Option 3a. The same as Option 3 but with a return type of `java.util.Optional<Boolean>` and returns `Optional.empty()` by default.

Option 4. ??? Leave suggestions below


Discussion: There's a lot more necessary than just adding that method. When I originally looked into that for the fluid API, there were a number of interactions with determining how bubble columns propagate within a single fluid and across multiple, which is tightly coupled along with the information being attached to a specific block to create the columns.



Fix (patch):
diff --git a/patches/net/minecraft/world/level/block/BubbleColumnBlock.java.patch b/patches/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
new file mode 100644
index 00000000000..526c55e206d
--- /dev/null
+++ b/patches/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/world/level/block/BubbleColumnBlock.java
++++ b/net/minecraft/world/level/block/BubbleColumnBlock.java
+@@ -121,10 +_,12 @@
+     private static BlockState getColumnState(BlockState p_152718_) {
+         if (p_152718_.is(Blocks.BUBBLE_COLUMN)) {
+             return p_152718_;
+-        } else if (p_152718_.is(Blocks.SOUL_SAND)) {
++        }
++        net.neoforged.neoforge.common.enums.BubbleColumnDirection bubbleColumnDirection = p_152718_.getBubbleColumnDirection(); // Neo: PR #931
++        if (bubbleColumnDirection == net.neoforged.neoforge.common.enums.BubbleColumnDirection.UPWARD) {
+             return Blocks.BUBBLE_COLUMN.defaultBlockState().setValue(DRAG_DOWN, Boolean.valueOf(false));
+         } else {
+-            return p_152718_.is(Blocks.MAGMA_BLOCK)
++            return bubbleColumnDirection == net.neoforged.neoforge.common.enums.BubbleColumnDirection.DOWNWARD
+                 ? Blocks.BUBBLE_COLUMN.defaultBlockState().setValue(DRAG_DOWN, Boolean.valueOf(true))
+                 : Blocks.WATER.defaultBlockState();
+         }
+@@ -190,7 +_,7 @@
+     @Override
+     protected boolean canSurvive(BlockState p_50986_, LevelReader p_50987_, BlockPos p_50988_) {
+         BlockState blockstate = p_50987_.getBlockState(p_50988_.below());
+-        return blockstate.is(Blocks.BUBBLE_COLUMN) || blockstate.is(Blocks.MAGMA_BLOCK) || blockstate.is(Blocks.SOUL_SAND);
++        return blockstate.is(Blocks.BUBBLE_COLUMN) || blockstate.getBubbleColumnDirection() != net.neoforged.neoforge.common.enums.BubbleColumnDirection.NONE; // Neo: PR #931
+     }
+ 
+     @Override
diff --git a/src/main/java/net/neoforged/neoforge/common/enums/BubbleColumnDirection.java b/src/main/java/net/neoforged/neoforge/common/enums/BubbleColumnDirection.java
new file mode 100644
index 00000000000..f4c3a3af5b4
--- /dev/null
+++ b/src/main/java/net/neoforged/neoforge/common/enums/BubbleColumnDirection.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.neoforged.neoforge.common.enums;
+
+public enum BubbleColumnDirection {
+    UPWARD,
+    DOWNWARD,
+    NONE
+}
diff --git a/src/main/java/net/neoforged/neoforge/common/enums/package-info.java b/src/main/java/net/neoforged/neoforge/common/enums/package-info.java
new file mode 100644
index 00000000000..4629e47c07b
--- /dev/null
+++ b/src/main/java/net/neoforged/neoforge/common/enums/package-info.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+@FieldsAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+@ParametersAreNonnullByDefault
+package net.neoforged.neoforge.common.enums;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+import net.minecraft.FieldsAreNonnullByDefault;
+import net.minecraft.MethodsReturnNonnullByDefault;
diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java
index 8be04a8f307..14a87bc0747 100644
--- a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java
+++ b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java
@@ -75,6 +75,7 @@
 import net.neoforged.neoforge.common.IPlantable;
 import net.neoforged.neoforge.common.ToolAction;
 import net.neoforged.neoforge.common.ToolActions;
+import net.neoforged.neoforge.common.enums.BubbleColumnDirection;
 import net.neoforged.neoforge.common.world.AuxiliaryLightManager;
 import org.jetbrains.annotations.Nullable;
 
@@ -976,4 +977,23 @@ default PushReaction getPistonPushReaction(BlockState state) {
     default boolean isEmpty(BlockState state) {
         return state.is(Blocks.AIR) || state.is(Blocks.CAVE_AIR) || state.is(Blocks.VOID_AIR);
     }
+
+    /**
+     * Determines if this block can spawn Bubble Columns and if so, what direction the column flows.
+     * <p></p>
+     * NOTE: The block itself will still need to call {@link net.minecraft.world.level.block.BubbleColumnBlock#updateColumn(LevelAccessor, BlockPos, BlockState)} in their tick method and schedule a block tick in the block's onPlace.
+     * Also, schedule a fluid tick in updateShape method if update direction is up. Both are needed in order to get the Bubble Columns to function properly. See {@link net.minecraft.world.level.block.SoulSandBlock} and {@link net.minecraft.world.level.block.MagmaBlock} for example.
+     *
+     * @param state The current state
+     * @return BubbleColumnDirection.NONE for no Bubble Column. Otherwise, will spawn Bubble Column flowing with specified direction
+     */
+    default BubbleColumnDirection getBubbleColumnDirection(BlockState state) {
+        if (state.is(Blocks.SOUL_SAND)) {
+            return BubbleColumnDirection.UPWARD;
+        } else if (state.is(Blocks.MAGMA_BLOCK)) {
+            return BubbleColumnDirection.DOWNWARD;
+        } else {
+            return BubbleColumnDirection.NONE;
+        }
+    }
 }
diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java
index ea41a4f2175..aee4f6741c0 100644
--- a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java
+++ b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java
@@ -41,6 +41,7 @@
 import net.neoforged.neoforge.common.IPlantable;
 import net.neoforged.neoforge.common.ToolAction;
 import net.neoforged.neoforge.common.ToolActions;
+import net.neoforged.neoforge.common.enums.BubbleColumnDirection;
 import net.neoforged.neoforge.common.world.AuxiliaryLightManager;
 import net.neoforged.neoforge.event.EventHooks;
 import org.jetbrains.annotations.Nullable;
@@ -746,4 +747,16 @@ default BlockState getAppearance(BlockAndTintGetter level, BlockPos pos, Directi
     default boolean isEmpty() {
         return self().getBlock().isEmpty(self());
     }
+
+    /**
+     * Determines if this block can spawn Bubble Columns and if so, what direction the column flows.
+     * <p></p>
+     * NOTE: The block itself will still need to call {@link net.minecraft.world.level.block.BubbleColumnBlock#updateColumn(LevelAccessor, BlockPos, BlockState)} in their tick method and schedule a block tick in the block's onPlace.
+     * Also, schedule a fluid tick in updateShape method if update direction is up. Both are needed in order to get the Bubble Columns to function properly. See {@link net.minecraft.world.level.block.SoulSandBlock} and {@link net.minecraft.world.level.block.MagmaBlock} for example.
+     *
+     * @return BubbleColumnDirection.NONE for no Bubble Column. Otherwise, will spawn Bubble Column flowing with specified direction
+     */
+    default BubbleColumnDirection getBubbleColumnDirection() {
+        return self().getBlock().getBubbleColumnDirection(self());
+    }
 }