From 2259220c93793d287650c0c5a9e91cfb89061729 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:15:28 +0100 Subject: [PATCH 1/6] Implement fluid handler support for cauldrons --- .../block/AbstractCauldronBlock.java.patch | 24 +++ .../neoforge/common/NeoForgeMod.java | 2 + .../neoforge/fluids/CauldronFluidContent.java | 169 ++++++++++++++++++ .../RegisterCauldronFluidContentEvent.java | 48 +++++ .../capability/wrappers/CauldronWrapper.java | 139 ++++++++++++++ .../neoforge/internal/RegistrationEvents.java | 4 +- .../capabilities/VanillaHandlersTests.java | 146 +++++++++++++++ .../capabilities/CapabilitiesTest.java | 23 --- .../capabilities/VanillaItemHandlerTests.java | 69 ------- tests/src/main/resources/META-INF/mods.toml | 2 - 10 files changed, 531 insertions(+), 95 deletions(-) create mode 100644 patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch create mode 100644 src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java create mode 100644 src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java create mode 100644 src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java create mode 100644 tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java delete mode 100644 tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/CapabilitiesTest.java delete mode 100644 tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/VanillaItemHandlerTests.java 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..2cc9a096a6 --- /dev/null +++ b/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch @@ -0,0 +1,24 @@ +--- a/net/minecraft/world/level/block/AbstractCauldronBlock.java ++++ b/net/minecraft/world/level/block/AbstractCauldronBlock.java +@@ -102,4 +_,21 @@ + + 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_) { ++ // 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/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 8e5337efee..a7392a6d5d 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..f9552bce04 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java @@ -0,0 +1,169 @@ +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, 1000, null); + registerEvent.register(Blocks.WATER_CAULDRON, Fluids.WATER, 1000, LayeredCauldronBlock.LEVEL); + registerEvent.register(Blocks.LAVA_CAULDRON, Fluids.LAVA, 1000, 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..3c124ad179 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java @@ -0,0 +1,48 @@ +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..3d3194fb64 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java @@ -0,0 +1,139 @@ +package net.neoforged.neoforge.fluids.capability.wrappers; + +import com.google.common.math.IntMath; +import java.util.Objects; +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.immutable(); + } + + @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 insertedIncrements = Math.min(resource.getAmount() / amountIncrements, (insertContent.maxLevel - currentContent.currentLevel(state)) / levelIncrements); + if (insertedIncrements > 0) { + updateLevel(insertContent, currentContent.currentLevel(state) + insertedIncrements * levelIncrements, action); + } + + return insertedIncrements * amountIncrements; + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + FluidStack current = getFluidInTank(0); + if (current.isFluidEqual(resource) && Objects.equals(current.getTag(), resource.getTag())) { + return drain(resource.getAmount(), action); + } else { + return FluidStack.EMPTY; + } + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + if (maxDrain <= 0) { + return FluidStack.EMPTY; + } + + BlockState state = level.getBlockState(pos); + 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 extractedIncrements = Math.min(maxDrain / amountIncrements, content.currentLevel(state) / levelIncrements); + if (extractedIncrements > 0) { + int newLevel = content.currentLevel(state) - extractedIncrements * levelIncrements; + if (newLevel == 0) { + // Fully extract -> back to empty cauldron + level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState()); + } else { + // Otherwise just decrease levels + updateLevel(content, content.currentLevel(state) - extractedIncrements * levelIncrements, 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..3eca70616a --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java @@ -0,0 +1,146 @@ +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.eventtest.internal.TestsMod; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.testframework.DynamicTest; +import net.neoforged.testframework.annotation.ForEachTest; +import net.neoforged.testframework.annotation.TestHolder; +import org.apache.commons.lang3.mutable.MutableInt; + +@ForEachTest(groups = "capabilities.vanillahandlers") +public class VanillaHandlersTests { + @TestHolder(description = { + "Tests that composter capabilities get invalidated correctly" + }) + @GameTest(template = TestsMod.TEMPLATE_3x3) + public static void testComposterInvalidation(DynamicTest test) { + test.onGameTest(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(); + }); + } + + @TestHolder(description = "Test cauldron interactions via the fluid handler capability") + @GameTest(template = TestsMod.TEMPLATE_3x3) + public static void testCauldronCapability(DynamicTest test) { + test.onGameTest(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); + + // 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/oldtest/capabilities/CapabilitiesTest.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/CapabilitiesTest.java deleted file mode 100644 index 4f50b5c720..0000000000 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/CapabilitiesTest.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.oldtest.capabilities; - -import net.neoforged.bus.api.IEventBus; -import net.neoforged.fml.common.Mod; -import net.neoforged.neoforge.event.RegisterGameTestsEvent; - -@Mod(CapabilitiesTest.MODID) -public class CapabilitiesTest { - public static final String MODID = "capabilities_test"; - - public CapabilitiesTest(IEventBus modBus) { - modBus.addListener(CapabilitiesTest::registerGameTests); - } - - private static void registerGameTests(RegisterGameTestsEvent event) { - event.register(VanillaItemHandlerTests.class); - } -} diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/VanillaItemHandlerTests.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/VanillaItemHandlerTests.java deleted file mode 100644 index 7783583d36..0000000000 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/capabilities/VanillaItemHandlerTests.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.oldtest.capabilities; - -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.gametest.framework.GameTest; -import net.minecraft.gametest.framework.GameTestHelper; -import net.minecraft.world.level.block.Blocks; -import net.neoforged.neoforge.capabilities.BlockCapabilityCache; -import net.neoforged.neoforge.capabilities.Capabilities; -import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; -import org.apache.commons.lang3.mutable.MutableInt; - -@PrefixGameTestTemplate(false) -public class VanillaItemHandlerTests { - @GameTest(templateNamespace = CapabilitiesTest.MODID, template = "empty3x3x3") - public static void testComposterInvalidation(GameTestHelper 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(); - } -} diff --git a/tests/src/main/resources/META-INF/mods.toml b/tests/src/main/resources/META-INF/mods.toml index 081bdff82f..6451ae9a63 100644 --- a/tests/src/main/resources/META-INF/mods.toml +++ b/tests/src/main/resources/META-INF/mods.toml @@ -156,8 +156,6 @@ modId="permissiontest" [[mods]] modId="part_entity_test" [[mods]] -modId="capabilities_test" -[[mods]] modId="custom_mob_bucket_test" [[mods]] modId="custom_armor_model_test" From aed6d0b8381b21cbc1bcf564493474a2fb655aab Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:43:47 +0100 Subject: [PATCH 2/6] Licenses! --- .../neoforged/neoforge/fluids/CauldronFluidContent.java | 5 +++++ .../neoforge/fluids/RegisterCauldronFluidContentEvent.java | 5 +++++ .../fluids/capability/wrappers/CauldronWrapper.java | 5 +++++ .../neoforge/debug/capabilities/VanillaHandlersTests.java | 7 +++++-- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java index f9552bce04..a332ba043b 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java +++ b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforge.fluids; import java.util.Collection; diff --git a/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java b/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java index 3c124ad179..57c5bb3845 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java +++ b/src/main/java/net/neoforged/neoforge/fluids/RegisterCauldronFluidContentEvent.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforge.fluids; import java.util.Objects; 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 index 3d3194fb64..0a60f9cdd8 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java +++ b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java @@ -1,3 +1,8 @@ +/* + * 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; 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 index 0e1f758d97..9c09ae9bc1 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java @@ -1,3 +1,8 @@ +/* + * 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; @@ -11,9 +16,7 @@ import net.minecraft.world.level.material.Fluids; import net.neoforged.neoforge.capabilities.BlockCapabilityCache; import net.neoforged.neoforge.capabilities.Capabilities; -import net.neoforged.neoforge.eventtest.internal.TestsMod; import net.neoforged.neoforge.fluids.FluidStack; -import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.gametest.EmptyTemplate; From 79836c35a9fb9315e6e5dfa820bfc6ff02948b06 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:49:31 +0100 Subject: [PATCH 3/6] Fixes --- .../block/AbstractCauldronBlock.java.patch | 3 +- .../capabilities/BlockCapability.java | 3 ++ .../neoforge/fluids/CauldronFluidContent.java | 6 ++-- .../capability/wrappers/CauldronWrapper.java | 34 +++++++++++++------ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch b/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch index 2cc9a096a6..3ca300683b 100644 --- a/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch +++ b/patches/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch @@ -1,12 +1,13 @@ --- a/net/minecraft/world/level/block/AbstractCauldronBlock.java +++ b/net/minecraft/world/level/block/AbstractCauldronBlock.java -@@ -102,4 +_,21 @@ +@@ -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_); diff --git a/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java b/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java index 564283229f..52b7aa223c 100644 --- a/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java +++ b/src/main/java/net/neoforged/neoforge/capabilities/BlockCapability.java @@ -137,6 +137,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/fluids/CauldronFluidContent.java b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java index a332ba043b..a2f0ea44fe 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java +++ b/src/main/java/net/neoforged/neoforge/fluids/CauldronFluidContent.java @@ -108,9 +108,9 @@ public static CauldronFluidContent getForFluid(Fluid fluid) { public static void init() { var registerEvent = new RegisterCauldronFluidContentEvent(); // Vanilla registrations - registerEvent.register(Blocks.CAULDRON, Fluids.EMPTY, 1000, null); - registerEvent.register(Blocks.WATER_CAULDRON, Fluids.WATER, 1000, LayeredCauldronBlock.LEVEL); - registerEvent.register(Blocks.LAVA_CAULDRON, Fluids.LAVA, 1000, null); + 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); } 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 index 0a60f9cdd8..8056c808ae 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java +++ b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java @@ -24,7 +24,7 @@ public class CauldronWrapper implements IFluidHandler { public CauldronWrapper(Level level, BlockPos pos) { this.level = level; - this.pos = pos.immutable(); + this.pos = pos; } @Override @@ -95,9 +95,10 @@ public int fill(FluidStack resource, FluidAction action) { int amountIncrements = insertContent.totalAmount / d; int levelIncrements = insertContent.maxLevel / d; - int insertedIncrements = Math.min(resource.getAmount() / amountIncrements, (insertContent.maxLevel - currentContent.currentLevel(state)) / levelIncrements); + int currentLevel = currentContent.currentLevel(state); + int insertedIncrements = Math.min(resource.getAmount() / amountIncrements, (insertContent.maxLevel - currentLevel) / levelIncrements); if (insertedIncrements > 0) { - updateLevel(insertContent, currentContent.currentLevel(state) + insertedIncrements * levelIncrements, action); + updateLevel(insertContent, currentLevel + insertedIncrements * levelIncrements, action); } return insertedIncrements * amountIncrements; @@ -105,21 +106,29 @@ public int fill(FluidStack resource, FluidAction action) { @Override public FluidStack drain(FluidStack resource, FluidAction action) { - FluidStack current = getFluidInTank(0); - if (current.isFluidEqual(resource) && Objects.equals(current.getTag(), resource.getTag())) { - return drain(resource.getAmount(), action); + if (resource.isEmpty()) { + return FluidStack.EMPTY; + } + + BlockState state = level.getBlockState(pos); + if (getContent(state).fluid == resource.getFluid() && null == resource.getTag()) { + return drain(state, resource.getAmount(), action); } else { return FluidStack.EMPTY; } } + @Override public FluidStack drain(int maxDrain, FluidAction action) { if (maxDrain <= 0) { return FluidStack.EMPTY; } - BlockState state = level.getBlockState(pos); + 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. @@ -127,15 +136,18 @@ public FluidStack drain(int maxDrain, FluidAction action) { int amountIncrements = content.totalAmount / d; int levelIncrements = content.maxLevel / d; - int extractedIncrements = Math.min(maxDrain / amountIncrements, content.currentLevel(state) / levelIncrements); + int currentLevel = content.currentLevel(state); + int extractedIncrements = Math.min(maxDrain / amountIncrements, currentLevel / levelIncrements); if (extractedIncrements > 0) { - int newLevel = content.currentLevel(state) - extractedIncrements * levelIncrements; + int newLevel = currentLevel - extractedIncrements * levelIncrements; if (newLevel == 0) { // Fully extract -> back to empty cauldron - level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState()); + if (action.execute()) { + level.setBlockAndUpdate(pos, Blocks.CAULDRON.defaultBlockState()); + } } else { // Otherwise just decrease levels - updateLevel(content, content.currentLevel(state) - extractedIncrements * levelIncrements, action); + updateLevel(content, newLevel, action); } } From c23985d0b4c7d7580d461e9c7f84f605cfc64800 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:53:12 +0100 Subject: [PATCH 4/6] No spots --- .../neoforge/fluids/capability/wrappers/CauldronWrapper.java | 2 -- 1 file changed, 2 deletions(-) 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 index 8056c808ae..929837ca79 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java +++ b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java @@ -6,7 +6,6 @@ package net.neoforged.neoforge.fluids.capability.wrappers; import com.google.common.math.IntMath; -import java.util.Objects; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; @@ -118,7 +117,6 @@ public FluidStack drain(FluidStack resource, FluidAction action) { } } - @Override public FluidStack drain(int maxDrain, FluidAction action) { if (maxDrain <= 0) { From 44a3c29aa24e1db3a389fabe9e9edcc2296dc470 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:28:25 +0100 Subject: [PATCH 5/6] hasTag --- .../neoforge/fluids/capability/wrappers/CauldronWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 929837ca79..d5df96a3f4 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java +++ b/src/main/java/net/neoforged/neoforge/fluids/capability/wrappers/CauldronWrapper.java @@ -110,7 +110,7 @@ public FluidStack drain(FluidStack resource, FluidAction action) { } BlockState state = level.getBlockState(pos); - if (getContent(state).fluid == resource.getFluid() && null == resource.getTag()) { + if (getContent(state).fluid == resource.getFluid() && !resource.hasTag()) { return drain(state, resource.getAmount(), action); } else { return FluidStack.EMPTY; From 89d2c102ac54935658252fcbeedc1b02207e7e18 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:45:31 +0100 Subject: [PATCH 6/6] Expect empty handler --- .../neoforge/debug/capabilities/VanillaHandlersTests.java | 1 + 1 file changed, 1 insertion(+) 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 index 9c09ae9bc1..9d3aeb124b 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/capabilities/VanillaHandlersTests.java @@ -123,6 +123,7 @@ public static void testCauldronCapability(ExtendedGameTestHelper helper) { 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);