diff --git a/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch b/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch new file mode 100644 index 0000000000..3ca300683b --- /dev/null +++ b/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch @@ -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 ++ if (net.neoforged.neoforge.fluids.CauldronFluidContent.getForBlock(p_51981_.getBlock()) == null) { ++ 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) { ++ p_60516_.invalidateCapabilities(p_60517_); ++ } ++ } + } diff --git a/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java b/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java index cde4fa0235..2d45c45035 100644 --- a/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java +++ b/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java @@ -139,6 +139,9 @@ private BlockCapability(ResourceLocation name, Class typeClass, Class 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) diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 3e7225f712..93f8ac8f14 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -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; @@ -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); diff --git a/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java new file mode 100644 index 0000000000..a2f0ea44fe --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java @@ -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. + * + *

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}. + * + *

The {@code CauldronFluidContent} itself defines: + *

+ */ +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_TO_CAULDRON = new IdentityHashMap<>(); + private static final Map 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)); + } + 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 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); + } + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java b/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java new file mode 100644 index 0000000000..57c5bb3845 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java @@ -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. + * + *

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. + * + *

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); + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java new file mode 100644 index 0000000000..d5df96a3f4 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.capability.wrappers; + +import com.google.common.math.IntMath; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.fluids.CauldronFluidContent; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.capability.IFluidHandler; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class CauldronWrapper implements IFluidHandler { + private final Level level; + private final BlockPos pos; + + public CauldronWrapper(Level level, BlockPos pos) { + this.level = level; + this.pos = pos; + } + + @Override + public int getTanks() { + return 1; + } + + private CauldronFluidContent getContent(BlockState state) { + CauldronFluidContent content = CauldronFluidContent.getForBlock(state.getBlock()); + if (content == null) { + throw new IllegalStateException("Unexpected error: no cauldron at location " + pos); + } + return content; + } + + @Override + public FluidStack getFluidInTank(int tank) { + BlockState state = level.getBlockState(pos); + CauldronFluidContent contents = getContent(state); + return new FluidStack(contents.fluid, contents.totalAmount * contents.currentLevel(state) / contents.maxLevel); + } + + @Override + public int getTankCapacity(int tank) { + BlockState state = level.getBlockState(pos); + CauldronFluidContent contents = getContent(state); + return contents.totalAmount; + } + + @Override + public boolean isFluidValid(int tank, FluidStack stack) { + return CauldronFluidContent.getForFluid(stack.getFluid()) != null; + } + + // Called by fill and drain to update the block state. + private void updateLevel(CauldronFluidContent newContent, int level, FluidAction action) { + if (action.execute()) { + BlockState newState = newContent.block.defaultBlockState(); + + if (newContent.levelProperty != null) { + newState = newState.setValue(newContent.levelProperty, level); + } + + this.level.setBlockAndUpdate(pos, newState); + } + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + if (resource.isEmpty()) { + return 0; + } + + CauldronFluidContent insertContent = CauldronFluidContent.getForFluid(resource.getFluid()); + if (insertContent == null) { + return 0; + } + + BlockState state = level.getBlockState(pos); + CauldronFluidContent currentContent = getContent(state); + if (currentContent.fluid != Fluids.EMPTY && currentContent.fluid != resource.getFluid()) { + // Fluid mismatch + return 0; + } + + // We can only insert increments based on the GCD between the number of levels and the total amount. + int d = IntMath.gcd(insertContent.maxLevel, insertContent.totalAmount); + int amountIncrements = insertContent.totalAmount / d; + int levelIncrements = insertContent.maxLevel / d; + + int currentLevel = currentContent.currentLevel(state); + int insertedIncrements = Math.min(resource.getAmount() / amountIncrements, (insertContent.maxLevel - currentLevel) / levelIncrements); + if (insertedIncrements > 0) { + updateLevel(insertContent, currentLevel + insertedIncrements * levelIncrements, action); + } + + return insertedIncrements * amountIncrements; + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + if (resource.isEmpty()) { + return FluidStack.EMPTY; + } + + BlockState state = level.getBlockState(pos); + if (getContent(state).fluid == resource.getFluid() && !resource.hasTag()) { + return drain(state, resource.getAmount(), action); + } else { + return FluidStack.EMPTY; + } + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + if (maxDrain <= 0) { + return FluidStack.EMPTY; + } + + return drain(level.getBlockState(pos), maxDrain, action); + } + + private FluidStack drain(BlockState state, int maxDrain, FluidAction action) { + CauldronFluidContent content = getContent(state); + + // We can only extract increments based on the GCD between the number of levels and the total amount. + int d = IntMath.gcd(content.maxLevel, content.totalAmount); + int amountIncrements = content.totalAmount / d; + int levelIncrements = content.maxLevel / d; + + int currentLevel = content.currentLevel(state); + int extractedIncrements = Math.min(maxDrain / amountIncrements, currentLevel / levelIncrements); + if (extractedIncrements > 0) { + int newLevel = currentLevel - extractedIncrements * levelIncrements; + if (newLevel == 0) { + // Fully extract -> back to empty cauldron + if (action.execute()) { + level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState()); + } + } else { + // Otherwise just decrease levels + updateLevel(content, newLevel, action); + } + } + + return new FluidStack(content.fluid, extractedIncrements * amountIncrements); + } +} diff --git a/src/main/java/net/neoforged/neoforge/internal/RegistrationEvents.java b/src/main/java/net/neoforged/neoforge/internal/RegistrationEvents.java index 5d1a3eba36..6273fb11ac 100644 --- a/src/main/java/net/neoforged/neoforge/internal/RegistrationEvents.java +++ b/src/main/java/net/neoforged/neoforge/internal/RegistrationEvents.java @@ -7,10 +7,12 @@ import net.neoforged.neoforge.capabilities.CapabilityHooks; import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager; +import net.neoforged.neoforge.fluids.CauldronFluidContent; class RegistrationEvents { public static void init() { - CapabilityHooks.init(); + CauldronFluidContent.init(); // must be before capability event + CapabilityHooks.init(); // must be after cauldron event ForcedChunkManager.init(); } } diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java new file mode 100644 index 0000000000..9d3aeb124b --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.debug.capabilities; + +import static net.neoforged.neoforge.fluids.capability.IFluidHandler.FluidAction.EXECUTE; +import static net.neoforged.neoforge.fluids.capability.IFluidHandler.FluidAction.SIMULATE; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LayeredCauldronBlock; +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.capabilities.BlockCapabilityCache; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.testframework.annotation.ForEachTest; +import net.neoforged.testframework.annotation.TestHolder; +import net.neoforged.testframework.gametest.EmptyTemplate; +import net.neoforged.testframework.gametest.ExtendedGameTestHelper; +import org.apache.commons.lang3.mutable.MutableInt; + +@ForEachTest(groups = "capabilities.vanillahandlers") +public class VanillaHandlersTests { + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests that composter capabilities get invalidated correctly") + public static void testComposterInvalidation(ExtendedGameTestHelper helper) { + var composterPos = new BlockPos(1, 1, 1); + + MutableInt invalidationCount = new MutableInt(); + var capCache = BlockCapabilityCache.create( + Capabilities.ItemHandler.BLOCK, + helper.getLevel(), + helper.absolutePos(composterPos), + Direction.UP, + () -> true, + invalidationCount::increment); + + if (capCache.getCapability() != null) + helper.fail("Expected no capability", composterPos); + if (capCache.getCapability() != null) // check again just in case + helper.fail("Expected no capability", composterPos); + if (invalidationCount.getValue() != 0) + helper.fail("Should not have been invalidated yet", composterPos); + + // The cache should only be invalidated once until it is queried again + helper.setBlock(composterPos, Blocks.COMPOSTER.defaultBlockState()); + if (invalidationCount.getValue() != 1) + helper.fail("Should have invalidated once"); + + helper.setBlock(composterPos, Blocks.AIR.defaultBlockState()); + if (invalidationCount.getValue() != 1) // capability not re-queried, so no invalidation + helper.fail("Should have invalidated once"); + + helper.setBlock(composterPos, Blocks.COMPOSTER.defaultBlockState()); + if (invalidationCount.getValue() != 1) // capability not re-queried, so no invalidation + helper.fail("Should have invalidated once"); + + // Should be ok to query now + if (capCache.getCapability() == null) + helper.fail("Expected capability", composterPos); + if (invalidationCount.getValue() != 1) + helper.fail("Should have invalidated once"); + + // Should be notified of disappearance if the composter is removed + helper.setBlock(composterPos, Blocks.AIR.defaultBlockState()); + + if (invalidationCount.getValue() != 2) + helper.fail("Should have invalidated a second time"); + if (capCache.getCapability() != null) + helper.fail("Expected no capability", composterPos); + + helper.succeed(); + } + + @GameTest + @EmptyTemplate + @TestHolder(description = "Test cauldron interactions via the fluid handler capability") + public static void testCauldronCapability(ExtendedGameTestHelper helper) { + var cauldronPos = new BlockPos(1, 1, 1); + + MutableInt invalidationCount = new MutableInt(); + var capCache = BlockCapabilityCache.create( + Capabilities.FluidHandler.BLOCK, + helper.getLevel(), + helper.absolutePos(cauldronPos), + Direction.UP, + () -> true, + invalidationCount::increment); + + // Capability should be absent + helper.assertTrue(capCache.getCapability() == null, "Expected no capability"); + + // Should invalidate once when setting the block + helper.setBlock(cauldronPos, Blocks.CAULDRON); + var wrapper = capCache.getCapability(); + helper.assertTrue(wrapper != null, "Expected fluid handler"); + helper.assertTrue(invalidationCount.intValue() == 1, "Expected 1 invalidation only"); + + helper.assertTrue(wrapper.getTanks() == 1, "Got %d tanks".formatted(wrapper.getTanks())); + + // Simulate filling with water + var fillResult = wrapper.fill(new FluidStack(Fluids.WATER, 2000), SIMULATE); + helper.assertTrue(fillResult == 1000, "Filled " + fillResult); + helper.assertBlockPresent(Blocks.CAULDRON, cauldronPos); + // Can't fill with less than 1000 though... + helper.assertTrue(wrapper.fill(new FluidStack(Fluids.WATER, 999), SIMULATE) == 0, "Expected 0 fill result"); + + // Action! + fillResult = wrapper.fill(new FluidStack(Fluids.WATER, 2000), EXECUTE); + helper.assertTrue(fillResult == 1000, "Filled " + fillResult); + helper.assertBlockState(cauldronPos, state -> state.is(Blocks.WATER_CAULDRON) && state.getValue(LayeredCauldronBlock.LEVEL) == 3, () -> "Expected level 3 cauldron"); + + helper.assertTrue(wrapper.getFluidInTank(0).equals(new FluidStack(Fluids.WATER, 1000)), "Expected 1000 water"); + + // Try to empty as well + helper.assertTrue(wrapper.drain(new FluidStack(Fluids.LAVA, 1000), EXECUTE).isEmpty(), "Cannot drain lava"); + helper.assertTrue(wrapper.drain(new FluidStack(Fluids.WATER, 999), EXECUTE).isEmpty(), "Cannot drain less than 1000 water"); + helper.assertTrue(wrapper.drain(new FluidStack(Fluids.WATER, 1000), EXECUTE).equals(new FluidStack(Fluids.WATER, 1000)), "Expected drain of 1000 water"); + + helper.assertBlockPresent(Blocks.CAULDRON, cauldronPos); + helper.assertTrue(wrapper.getFluidInTank(0).isEmpty(), "Expected empty handler"); + + // Try lava cauldron + helper.setBlock(cauldronPos, Blocks.LAVA_CAULDRON); + helper.assertTrue(wrapper.getFluidInTank(0).equals(new FluidStack(Fluids.LAVA, 1000)), "Expected 1000 lava"); + helper.assertTrue(wrapper.drain(1000, EXECUTE).equals(new FluidStack(Fluids.LAVA, 1000)), "Expected drain of 1000 lava"); + helper.assertBlockPresent(Blocks.CAULDRON, cauldronPos); + + // Try partial water filling + helper.setBlock(cauldronPos, Blocks.WATER_CAULDRON.defaultBlockState().setValue(LayeredCauldronBlock.LEVEL, 2)); + helper.assertTrue(wrapper.getFluidInTank(0).equals(new FluidStack(Fluids.WATER, 666)), "Expected 666 water"); + helper.assertTrue(wrapper.drain(1000, EXECUTE).isEmpty(), "Expected no water drain from partial cauldron"); + helper.assertTrue(wrapper.fill(new FluidStack(Fluids.WATER, 1000), EXECUTE) == 0, "Expected no water fill to partial cauldron"); + + // None of this should have invalidated the capability + helper.assertTrue(invalidationCount.intValue() == 1, "Expected 1 invalidation only after the whole test"); + // But if we change the block to a non-cauldron, it should invalidate + helper.destroyBlock(cauldronPos); + helper.assertTrue(invalidationCount.intValue() == 2, "Expected a second invalidation after cauldron destruction"); + + helper.succeed(); + } +} diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/capability/CapabilityTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/capability/CapabilityTests.java deleted file mode 100644 index edf17574ac..0000000000 --- a/tests/src/main/java/net/neoforged/neoforge/debug/capability/CapabilityTests.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.debug.capability; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.gametest.framework.GameTest; -import net.minecraft.world.level.block.Blocks; -import net.neoforged.neoforge.capabilities.BlockCapabilityCache; -import net.neoforged.neoforge.capabilities.Capabilities; -import net.neoforged.testframework.annotation.ForEachTest; -import net.neoforged.testframework.annotation.TestHolder; -import net.neoforged.testframework.gametest.EmptyTemplate; -import net.neoforged.testframework.gametest.ExtendedGameTestHelper; -import org.apache.commons.lang3.mutable.MutableInt; - -@ForEachTest(groups = "capability") -public class CapabilityTests { - @GameTest - @EmptyTemplate - @TestHolder(description = "Tests if composter invalidation works") - static void composterInvalidationTest(final ExtendedGameTestHelper helper) { - var composterPos = new BlockPos(1, 1, 1); - - MutableInt invalidationCount = new MutableInt(); - var capCache = BlockCapabilityCache.create( - Capabilities.ItemHandler.BLOCK, - helper.getLevel(), - helper.absolutePos(composterPos), - Direction.UP, - () -> true, - invalidationCount::increment); - - if (capCache.getCapability() != null) - helper.fail("Expected no capability", composterPos); - if (capCache.getCapability() != null) // check again just in case - helper.fail("Expected no capability", composterPos); - if (invalidationCount.getValue() != 0) - helper.fail("Should not have been invalidated yet", composterPos); - - // The cache should only be invalidated once until it is queried again - helper.setBlock(composterPos, Blocks.COMPOSTER.defaultBlockState()); - if (invalidationCount.getValue() != 1) - helper.fail("Should have invalidated once"); - - helper.setBlock(composterPos, Blocks.AIR.defaultBlockState()); - if (invalidationCount.getValue() != 1) // capability not re-queried, so no invalidation - helper.fail("Should have invalidated once"); - - helper.setBlock(composterPos, Blocks.COMPOSTER.defaultBlockState()); - if (invalidationCount.getValue() != 1) // capability not re-queried, so no invalidation - helper.fail("Should have invalidated once"); - - // Should be ok to query now - if (capCache.getCapability() == null) - helper.fail("Expected capability", composterPos); - if (invalidationCount.getValue() != 1) - helper.fail("Should have invalidated once"); - - // Should be notified of disappearance if the composter is removed - helper.setBlock(composterPos, Blocks.AIR.defaultBlockState()); - - if (invalidationCount.getValue() != 2) - helper.fail("Should have invalidated a second time"); - if (capCache.getCapability() != null) - helper.fail("Expected no capability", composterPos); - - helper.succeed(); - } -}