diff --git a/src/main/java/xyz/nucleoid/stimuli/event/block/BlockFallEvent.java b/src/main/java/xyz/nucleoid/stimuli/event/block/BlockFallEvent.java
new file mode 100644
index 0000000..35622ba
--- /dev/null
+++ b/src/main/java/xyz/nucleoid/stimuli/event/block/BlockFallEvent.java
@@ -0,0 +1,36 @@
+package xyz.nucleoid.stimuli.event.block;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.math.BlockPos;
+import xyz.nucleoid.stimuli.event.StimulusEvent;
+
+/**
+ * Called when a gravity-affected block attempts to fall.
+ *
+ *
Upon return:
+ *
+ * - {@link ActionResult#SUCCESS} cancels further processing and allows the block to fall.
+ *
- {@link ActionResult#FAIL} cancels further processing and prevents the block from falling.
+ *
- {@link ActionResult#PASS} moves on to the next listener.
+ *
+ * If all listeners return {@link ActionResult#PASS}, the block successfully falls.
+ */
+public interface BlockFallEvent {
+ StimulusEvent EVENT = StimulusEvent.create(BlockFallEvent.class, ctx -> (world, pos, state) -> {
+ try {
+ for (BlockFallEvent listener : ctx.getListeners()) {
+ ActionResult result = listener.onBlockFall(world, pos, state);
+ if (result != ActionResult.PASS) {
+ return result;
+ }
+ }
+ } catch (Throwable t) {
+ ctx.handleException(t);
+ }
+ return ActionResult.PASS;
+ });
+
+ ActionResult onBlockFall(ServerWorld world, BlockPos pos, BlockState state);
+}
diff --git a/src/main/java/xyz/nucleoid/stimuli/mixin/block/FallingBlockMixin.java b/src/main/java/xyz/nucleoid/stimuli/mixin/block/FallingBlockMixin.java
new file mode 100644
index 0000000..677b138
--- /dev/null
+++ b/src/main/java/xyz/nucleoid/stimuli/mixin/block/FallingBlockMixin.java
@@ -0,0 +1,30 @@
+package xyz.nucleoid.stimuli.mixin.block;
+
+import java.util.Random;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.block.FallingBlock;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.math.BlockPos;
+import xyz.nucleoid.stimuli.EventInvokers;
+import xyz.nucleoid.stimuli.Stimuli;
+import xyz.nucleoid.stimuli.event.block.BlockFallEvent;
+
+@Mixin(FallingBlock.class)
+public class FallingBlockMixin {
+ @Inject(method = "scheduledTick", at = @At("HEAD"), cancellable = true)
+ private void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random random, CallbackInfo ci) {
+ try (EventInvokers invokers = Stimuli.select().at(world, pos)) {
+ ActionResult result = invokers.get(BlockFallEvent.EVENT).onBlockFall(world, pos, state);
+ if (result == ActionResult.FAIL) {
+ ci.cancel();
+ }
+ }
+ }
+}
diff --git a/src/main/resources/stimuli.mixins.json b/src/main/resources/stimuli.mixins.json
index cc8f45f..b1e02e2 100644
--- a/src/main/resources/stimuli.mixins.json
+++ b/src/main/resources/stimuli.mixins.json
@@ -6,6 +6,7 @@
"mixins": [
"block.BlockItemMixin",
"block.BlockMixin",
+ "block.FallingBlockMixin",
"block.FarmlandBlockMixin",
"entity.LivingEntityMixin",
"player.CraftingResultInventoryMixin",