Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement fluid handler support for cauldrons #369

Merged
merged 8 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--- a/net/minecraft/world/level/block/AbstractCauldronBlock.java
+++ b/net/minecraft/world/level/block/AbstractCauldronBlock.java
@@ -102,4 +_,22 @@

protected void receiveStalactiteDrip(BlockState p_151975_, Level p_151976_, BlockPos p_151977_, Fluid p_151978_) {
}
+
+ @Override
+ public void onPlace(BlockState p_51978_, Level p_51979_, BlockPos p_51980_, BlockState p_51981_, boolean p_51982_) {
+ super.onPlace(p_51978_, p_51979_, p_51980_, p_51981_, p_51982_);
+ // Neo: Invalidate cauldron capabilities when a cauldron is added
Technici4n marked this conversation as resolved.
Show resolved Hide resolved
+ if (net.neoforged.neoforge.fluids.CauldronFluidContent.getForBlock(p_51981_.getBlock()) == null) {
pupnewfster marked this conversation as resolved.
Show resolved Hide resolved
+ p_51979_.invalidateCapabilities(p_51980_);
+ }
+ }
+
+ @Override
+ public void onRemove(BlockState p_60515_, Level p_60516_, BlockPos p_60517_, BlockState p_60518_, boolean p_60519_) {
+ super.onRemove(p_60515_, p_60516_, p_60517_, p_60518_, p_60519_);
+ // Neo: Invalidate cauldron capabilities when a cauldron is removed
+ if (net.neoforged.neoforge.fluids.CauldronFluidContent.getForBlock(p_60518_.getBlock()) == null) {
pupnewfster marked this conversation as resolved.
Show resolved Hide resolved
+ p_60516_.invalidateCapabilities(p_60517_);
+ }
+ }
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ private BlockCapability(ResourceLocation name, Class<T> typeClass, Class<C> cont
@ApiStatus.Internal
@Nullable
public T getCapability(Level level, BlockPos pos, @Nullable BlockState state, @Nullable BlockEntity blockEntity, C context) {
// Convert pos to immutable, it's easy to forget otherwise
pos = pos.immutable();

// Get block state and block entity if they were not provided
if (blockEntity == null) {
if (state == null)
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
import net.neoforged.neoforge.data.event.GatherDataEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.fluids.BaseFlowingFluid;
import net.neoforged.neoforge.fluids.CauldronFluidContent;
import net.neoforged.neoforge.fluids.FluidType;
import net.neoforged.neoforge.forge.snapshots.ForgeSnapshotsMod;
import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion;
Expand Down Expand Up @@ -507,6 +508,7 @@ public NeoForgeMod(IEventBus modEventBus, Dist dist) {
DualStackUtils.initialise();

modEventBus.addListener(CapabilityHooks::registerVanillaProviders);
modEventBus.addListener(CauldronFluidContent::registerCapabilities);
// These 3 listeners use the default priority for now, can be re-evaluated later.
NeoForge.EVENT_BUS.addListener(CapabilityHooks::invalidateCapsOnChunkLoad);
NeoForge.EVENT_BUS.addListener(CapabilityHooks::invalidateCapsOnChunkUnload);
Expand Down
174 changes: 174 additions & 0 deletions src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.fluids;

import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LayeredCauldronBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.Fluids;
import net.neoforged.fml.ModLoader;
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import net.neoforged.neoforge.fluids.capability.wrappers.CauldronWrapper;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

/**
* Fluid content information for cauldrons.
*
* <p>Empty, water and lava cauldrons are registered by default,
* and additional cauldrons must be registered with {@link RegisterCauldronFluidContentEvent}.
* Contents can be queried with {@link #getForBlock} and {@link #getForFluid}.
*
* <p>The {@code CauldronFluidContent} itself defines:
* <ul>
* <li>The block of the cauldron.</li>
* <li>The fluid that can be accepted by the cauldron. NBT is discarded when entering the cauldron.</li>
* <li>Which fluid amounts can be stored in the cauldron, and how they map to the level property of the cauldron.
* If {@link #levelProperty} is {@code null}, then {@code maxLevel = 1}, and there is only one level.
* Otherwise, the levels are all the integer values between {@code 1} and {@link #maxLevel} (included).
* </li>
* <li>{@link #totalAmount} defines how much fluid (in millibuckets) there is in one level of the cauldron.</li>
* </ul>
*/
public final class CauldronFluidContent {
/**
* Block of the cauldron.
*/
public final Block block;
/**
* Fluid stored inside the cauldron.
*/
public final Fluid fluid;
/**
* Amount of {@code #fluid} in millibuckets in the entire full cauldron.
*/
public final int totalAmount;
/**
* Maximum level for {@link #levelProperty}. {@code 1} if {@code levelProperty} is null, otherwise a number {@code >= 1}.
* The minimum level is always 1.
*/
public final int maxLevel;
/**
* Property storing the level of the cauldron. If it's {@code null}, only one level is possible.
*/
@Nullable
public final IntegerProperty levelProperty;

/**
* Return the current level of the cauldron given its block state, or 0 if it's an empty cauldron.
*/
public int currentLevel(BlockState state) {
if (fluid == Fluids.EMPTY) {
return 0;
} else if (levelProperty == null) {
return 1;
} else {
return state.getValue(levelProperty);
}
}

private CauldronFluidContent(Block block, Fluid fluid, int totalAmount, int maxLevel, @Nullable IntegerProperty levelProperty) {
this.block = block;
this.fluid = fluid;
this.totalAmount = totalAmount;
this.maxLevel = maxLevel;
this.levelProperty = levelProperty;
}

private static final Map<Block, CauldronFluidContent> BLOCK_TO_CAULDRON = new IdentityHashMap<>();
private static final Map<Fluid, CauldronFluidContent> FLUID_TO_CAULDRON = new IdentityHashMap<>();

/**
* Get the cauldron fluid content for a cauldron block, or {@code null} if none was registered (yet).
*/
@Nullable
public static CauldronFluidContent getForBlock(Block block) {
return BLOCK_TO_CAULDRON.get(block);
}

/**
* Get the cauldron fluid content for a fluid, or {@code null} if no cauldron was registered for that fluid (yet).
*/
@Nullable
public static CauldronFluidContent getForFluid(Fluid fluid) {
return FLUID_TO_CAULDRON.get(fluid);
}

@ApiStatus.Internal
public static void init() {
var registerEvent = new RegisterCauldronFluidContentEvent();
// Vanilla registrations
registerEvent.register(Blocks.CAULDRON, Fluids.EMPTY, FluidType.BUCKET_VOLUME, null);
registerEvent.register(Blocks.WATER_CAULDRON, Fluids.WATER, FluidType.BUCKET_VOLUME, LayeredCauldronBlock.LEVEL);
registerEvent.register(Blocks.LAVA_CAULDRON, Fluids.LAVA, FluidType.BUCKET_VOLUME, null);
// Modded registrations
ModLoader.get().postEvent(registerEvent);
}

/**
* Do not try to call, use the {@link RegisterCauldronFluidContentEvent} event instead.
*/
static void register(Block block, Fluid fluid, int totalAmount, @Nullable IntegerProperty levelProperty) {
if (BLOCK_TO_CAULDRON.get(block) != null) {
throw new IllegalArgumentException("Duplicate cauldron registration for block %s.".formatted(block));
}
if (FLUID_TO_CAULDRON.get(fluid) != null) {
throw new IllegalArgumentException("Duplicate cauldron registration for fluid %s.".formatted(fluid));
}
pupnewfster marked this conversation as resolved.
Show resolved Hide resolved
if (totalAmount <= 0) {
throw new IllegalArgumentException("Cauldron total amount %d should be positive.".formatted(totalAmount));
}

CauldronFluidContent data;

if (levelProperty == null) {
data = new CauldronFluidContent(block, fluid, totalAmount, 1, null);
} else {
Collection<Integer> levels = levelProperty.getPossibleValues();
if (levels.isEmpty()) {
throw new IllegalArgumentException("Cauldron should have at least one possible level.");
}

int minLevel = Integer.MAX_VALUE;
int maxLevel = 0;

for (int level : levels) {
minLevel = Math.min(minLevel, level);
maxLevel = Math.max(maxLevel, level);
}

if (minLevel != 1) {
throw new IllegalStateException("Minimum level should be 1, and maximum level should be >= 1.");
}

data = new CauldronFluidContent(block, fluid, totalAmount, maxLevel, levelProperty);
}

BLOCK_TO_CAULDRON.put(block, data);
FLUID_TO_CAULDRON.put(fluid, data);
}

@ApiStatus.Internal
public static void registerCapabilities(RegisterCapabilitiesEvent event) {
if (BLOCK_TO_CAULDRON.isEmpty()) {
throw new IllegalStateException("CauldronFluidContent.init() should have been called before the capability event!");
}

for (Block block : BLOCK_TO_CAULDRON.keySet()) {
event.registerBlock(
Capabilities.FluidHandler.BLOCK,
(level, pos, state, be, context) -> new CauldronWrapper(level, pos),
block);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.fluids;

import java.util.Objects;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.AbstractCauldronBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.Fluid;
import net.neoforged.bus.api.Event;
import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.neoforge.capabilities.Capabilities;
import org.jetbrains.annotations.Nullable;

/**
* Event to register {@link CauldronFluidContent} for modded cauldrons.
*
* <p>Registering cauldrons is done by calling {@link CauldronFluidContent#register}
* and allows all cauldrons registered in this way to interoperate with each other
* when accessed via the {@link Capabilities.FluidHandler#BLOCK} capability.
*/
public class RegisterCauldronFluidContentEvent extends Event implements IModBusEvent {
RegisterCauldronFluidContentEvent() {}

/**
* Register a new cauldron, allowing it to be filled and emptied through the standard capability.
* In both cases, return the content of the cauldron, either the existing one, or the newly registered one.
*
* <p>If the block is not a subclass of {@link AbstractCauldronBlock},
* {@link BlockBehaviour#onPlace(BlockState, Level, BlockPos, BlockState, boolean)}
* and {@link BlockBehaviour#onRemove(BlockState, Level, BlockPos, BlockState, boolean)}
* must be overridden to invalidate capabilities when the block changes!
* See how NeoForge patches {@link AbstractCauldronBlock} for reference.
*
* @param block the block of the cauldron
* @param fluid the fluid stored in this cauldron
* @param totalAmount how much fluid can fit in the cauldron at maximum capacity, in {@linkplain FluidStack millibuckets}
* @param levelProperty the property used by the cauldron to store its levels, or {@code null} if the cauldron only has one level
*/
public void register(Block block, Fluid fluid, int totalAmount, @Nullable IntegerProperty levelProperty) {
Objects.requireNonNull(block, "Block may not be null");
Objects.requireNonNull(fluid, "Fluid may not be null");

CauldronFluidContent.register(block, fluid, totalAmount, levelProperty);
}
}
Loading
Loading