From 10cf12151c48fd236f7e90f1784c8edd1184c285 Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 17:34:32 -0500 Subject: [PATCH 1/2] Add a generic fluid capability that optionally applies to bottles. Optional in that if another mod has already registered a fluid capability for bottles, we do not double the effort. --- docs/fluid-bottles.md | 50 ++++++ src/main/java/zone/moddev/mc/lib/MMDLib.java | 4 + .../mc/lib/client/FluidBottleClient.java | 24 +++ .../common/fluid/EmptyBottleFluidHandler.java | 65 ++++++++ .../mc/lib/common/fluid/FluidBottleItem.java | 44 ++++++ .../mc/lib/common/fluid/FluidBottles.java | 147 ++++++++++++++++++ .../mc/lib/common/fluid/package-info.java | 5 + .../resources/assets/mmdlib/lang/en_us.json | 4 +- .../mmdlib/models/item/fluid_bottle.json | 11 ++ .../moddev/mc/lib/smoke/FluidBottleSmoke.java | 106 +++++++++++++ .../moddev/mc/lib/smoke/MMDLibGameTests.java | 15 ++ .../data/mmdlib/tags/fluid/bottleable.json | 4 + 12 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 docs/fluid-bottles.md create mode 100644 src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java create mode 100644 src/main/java/zone/moddev/mc/lib/common/fluid/EmptyBottleFluidHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottleItem.java create mode 100644 src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottles.java create mode 100644 src/main/java/zone/moddev/mc/lib/common/fluid/package-info.java create mode 100644 src/main/resources/assets/mmdlib/models/item/fluid_bottle.json create mode 100644 src/runtimeTest/java/zone/moddev/mc/lib/smoke/FluidBottleSmoke.java create mode 100644 src/runtimeTest/resources/data/mmdlib/tags/fluid/bottleable.json diff --git a/docs/fluid-bottles.md b/docs/fluid-bottles.md new file mode 100644 index 00000000..caf01604 --- /dev/null +++ b/docs/fluid-bottles.md @@ -0,0 +1,50 @@ +# Fluid bottles + +Vanilla glass bottles only pick up water. MMDLib lets them hold any fluid in the +`#mmdlib:bottleable` fluid tag, without a mixin. A filled bottle is a single item, +`mmdlib:fluid_bottle`, that holds **333 mB**, a third of a bucket, of one fluid. + +## Making a fluid bottleable + +Add its **source** fluid to the tag with a datapack or your mod's data: + +```json +// data/mmdlib/tags/fluid/bottleable.json +{ "replace": false, "values": ["example:molten_tin"] } +``` + +MMDLib ships the tag empty, so packs and mods decide what can be bottled. +Vanilla's own fluids (water, lava) are always left to vanilla, even if tagged: +a water bottle is still vanilla's water potion. + +## What works + +* **Right-click a source block** with a glass bottle to fill it. The source block + stays, as it does for water. Protected blocks (`Level.mayInteract`) are skipped. +* **Tanks and pipes** fill and empty bottles through the fluid item capability + (`Capabilities.FluidHandler.ITEM`), for example with `FluidUtil`. Filling takes + exactly 333 mB. Emptying a bottle leaves a glass bottle. +* **Name**: "Bottle of" plus the fluid's name, such as "Bottle of Molten Copper". +* **Look**: vanilla's potion bottle, with the fluid's still texture showing + through the potion's liquid area. It is tinted with whatever color the fluid + type registers on the client (`IClientFluidTypeExtensions#getTintColor`); + fluids without a tint show their texture as drawn. Glowing fluids glow. + +## Other mods' glass bottle capabilities + +If another mod already gives glass bottles a fluid capability, MMDLib doesn't +register its own: it logs that at startup and leaves filling glass bottles from +tanks to that mod. Right-clicking bottleable fluids, and filled MMDLib bottles, +work either way. MMDLib registers at the lowest priority, so mods registering at +the usual priority are seen first. + +## Code + +`zone.moddev.mc.lib.common.fluid.FluidBottles` holds the tag (`BOTTLEABLE`), the +capacity, the item (`BOTTLE`) and its fluid component (`CONTENT`). +`FluidBottleItem.filled(fluid)` makes a full bottle, and +`FluidBottleItem.contents(stack)` reads one. `FluidBottles.bottleable(fluid)` is +the single rule for what can be bottled. + +`FluidBottleSmoke` (game tests) checks filling from the world, water left to +vanilla, untagged fluids refused, and filling and emptying through tanks. diff --git a/src/main/java/zone/moddev/mc/lib/MMDLib.java b/src/main/java/zone/moddev/mc/lib/MMDLib.java index 0f5c9cf7..34831328 100644 --- a/src/main/java/zone/moddev/mc/lib/MMDLib.java +++ b/src/main/java/zone/moddev/mc/lib/MMDLib.java @@ -117,9 +117,13 @@ public MMDLib(IEventBus modBus, ModContainer container) { NeoForge.EVENT_BUS.addListener( zone.moddev.mc.lib.machine.neoforge.MachinePartsLoader::register); modBus.addListener(zone.moddev.mc.lib.util.Registries::register); +<<<<<<< Updated upstream if (ModList.get().isLoaded("crafttweaker")) CraftTweakerInit.register(modBus); +======= + zone.moddev.mc.lib.common.fluid.FluidBottles.register(modBus); +>>>>>>> Stashed changes modBus.addListener(this::publishDefinitions); modBus.addListener((FMLConstructModEvent event) -> event.enqueueWork(this::loadDefinitions)); new ItemEventBridge( diff --git a/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java b/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java new file mode 100644 index 00000000..b6ed69c8 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/client/FluidBottleClient.java @@ -0,0 +1,24 @@ +package zone.moddev.mc.lib.client; + +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; +import net.neoforged.neoforge.client.model.DynamicFluidContainerModel; + +import zone.moddev.mc.lib.common.fluid.FluidBottles; + +/** + * Colors the fluid in a {@code mmdlib:fluid_bottle} with whatever tint its fluid type registers on + * the client ({@code IClientFluidTypeExtensions#getTintColor}); fluids that register none show + * their texture as drawn. + */ +@EventBusSubscriber(modid = "mmdlib", bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) +public final class FluidBottleClient { + private FluidBottleClient() {} + + @SubscribeEvent + public static void colors(RegisterColorHandlersEvent.Item event) { + event.register(new DynamicFluidContainerModel.Colors(), FluidBottles.BOTTLE.get()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/common/fluid/EmptyBottleFluidHandler.java b/src/main/java/zone/moddev/mc/lib/common/fluid/EmptyBottleFluidHandler.java new file mode 100644 index 00000000..f4560ac8 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/common/fluid/EmptyBottleFluidHandler.java @@ -0,0 +1,65 @@ +package zone.moddev.mc.lib.common.fluid; + +import net.minecraft.world.item.ItemStack; + +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.capability.IFluidHandlerItem; + +/** + * The fluid capability of an empty glass bottle: it takes exactly {@link FluidBottles#CAPACITY} of + * a bottleable fluid at once and becomes a {@link FluidBottleItem}, the way NeoForge's bucket + * wrapper turns an empty bucket into a filled one. Anything else is refused, so other fluid + * handling of glass bottles is left alone. + */ +final class EmptyBottleFluidHandler implements IFluidHandlerItem { + private ItemStack container; + + EmptyBottleFluidHandler(ItemStack container) { + this.container = container; + } + + @Override + public ItemStack getContainer() { + return container; + } + + @Override + public int getTanks() { + return 1; + } + + @Override + public FluidStack getFluidInTank(int tank) { + return FluidStack.EMPTY; + } + + @Override + public int getTankCapacity(int tank) { + return FluidBottles.CAPACITY; + } + + @Override + public boolean isFluidValid(int tank, FluidStack stack) { + return FluidBottles.bottleable(stack.getFluid()); + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + // A single bottle, filled whole: a stack of bottles is filled one at a time by FluidUtil. + if (container.getCount() != 1 + || resource.getAmount() < FluidBottles.CAPACITY + || !FluidBottles.bottleable(resource.getFluid())) return 0; + if (action.execute()) container = FluidBottleItem.filled(resource.getFluid()); + return FluidBottles.CAPACITY; + } + + @Override + public FluidStack drain(FluidStack resource, FluidAction action) { + return FluidStack.EMPTY; + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + return FluidStack.EMPTY; + } +} diff --git a/src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottleItem.java b/src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottleItem.java new file mode 100644 index 00000000..d064fd5c --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottleItem.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.lib.common.fluid; + +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.material.Fluid; + +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.SimpleFluidContent; + +/** + * {@code mmdlib:fluid_bottle}: a glass bottle holding {@link FluidBottles#CAPACITY} of one fluid, + * kept in the {@link FluidBottles#CONTENT} component. Named after its fluid ("Bottle of Molten + * Copper"); emptying it, through its fluid capability, leaves a glass bottle. + */ +public final class FluidBottleItem extends Item { + /** One-high stacks, like potions: bottles of different fluids cannot share a stack anyway. */ + public FluidBottleItem(Properties properties) { + super(properties.stacksTo(1).craftRemainder(Items.GLASS_BOTTLE)); + } + + /** The fluid in {@code stack}; empty for an unfilled bottle. */ + public static FluidStack contents(ItemStack stack) { + return stack.getOrDefault(FluidBottles.CONTENT.get(), SimpleFluidContent.EMPTY).copy(); + } + + /** A bottle full of {@code fluid}. */ + public static ItemStack filled(Fluid fluid) { + var stack = new ItemStack(FluidBottles.BOTTLE.get()); + stack.set( + FluidBottles.CONTENT.get(), + SimpleFluidContent.copyOf(new FluidStack(fluid, FluidBottles.CAPACITY))); + return stack; + } + + @Override + public Component getName(ItemStack stack) { + var fluid = contents(stack); + return fluid.isEmpty() + ? super.getName(stack) + : Component.translatable(getDescriptionId(stack) + ".filled", fluid.getHoverName()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottles.java b/src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottles.java new file mode 100644 index 00000000..46ef750b --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottles.java @@ -0,0 +1,147 @@ +package zone.moddev.mc.lib.common.fluid; + +import java.util.function.Supplier; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stats; +import net.minecraft.tags.TagKey; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ItemUtils; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; + +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; +import net.neoforged.neoforge.fluids.SimpleFluidContent; +import net.neoforged.neoforge.fluids.capability.templates.FluidHandlerItemStack; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +import zone.moddev.mc.lib.MMDLib; + +/** + * Lets vanilla glass bottles hold any fluid in {@link #BOTTLEABLE}, without a mixin: + * + * + * + * Vanilla's own fluids (water, lava) are always left to vanilla, tagged or not. + */ +public final class FluidBottles { + private static final Logger LOGGER = LogUtils.getLogger(); + + /** Source fluids glass bottles can hold. MMDLib ships it empty: packs and mods fill it. */ + public static final TagKey BOTTLEABLE = + TagKey.create(Registries.FLUID, ResourceLocation.fromNamespaceAndPath(MMDLib.MODID, "bottleable")); + + /** Millibuckets in a bottle: a third of a bucket, the common convention. */ + public static final int CAPACITY = 333; + + private static final DeferredRegister.DataComponents COMPONENTS = + DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MMDLib.MODID); + + /** The fluid a {@link FluidBottleItem} holds. */ + public static final Supplier> CONTENT = + COMPONENTS.registerComponentType( + "bottle_fluid", + builder -> + builder + .persistent(SimpleFluidContent.CODEC) + .networkSynchronized(SimpleFluidContent.STREAM_CODEC)); + + /** {@code mmdlib:fluid_bottle}. */ + public static final DeferredHolder BOTTLE = + MMDLib.ITEMS.register( + "fluid_bottle", () -> new FluidBottleItem(new net.minecraft.world.item.Item.Properties())); + + private FluidBottles() {} + + /** Registers the component, the capabilities and the bottle-filling hook. */ + public static void register(IEventBus modBus) { + COMPONENTS.register(modBus); + // Lowest priority, so mods registering at the usual priority are seen first. + modBus.addListener(EventPriority.LOWEST, FluidBottles::capabilities); + NeoForge.EVENT_BUS.addListener(FluidBottles::fillFromWorld); + } + + /** Whether glass bottles can hold {@code fluid}: tagged, and not one of vanilla's. */ + public static boolean bottleable(Fluid fluid) { + return !BuiltInRegistries.FLUID.getKey(fluid).getNamespace().equals("minecraft") + && fluid.defaultFluidState().is(BOTTLEABLE); + } + + private static void capabilities(RegisterCapabilitiesEvent event) { + event.registerItem( + Capabilities.FluidHandler.ITEM, + (stack, context) -> + new FluidHandlerItemStack.SwapEmpty( + CONTENT, stack, new net.minecraft.world.item.ItemStack(Items.GLASS_BOTTLE), CAPACITY) { + @Override + public boolean canFillFluidType(net.neoforged.neoforge.fluids.FluidStack fluid) { + return bottleable(fluid.getFluid()); + } + }, + BOTTLE.get()); + if (event.isItemRegistered(Capabilities.FluidHandler.ITEM, Items.GLASS_BOTTLE)) { + LOGGER.info( + "Another mod already gives glass bottles a fluid capability; MMDLib leaves filling them" + + " from tanks to it. Right-clicking bottleable fluids still works."); + return; + } + event.registerItem( + Capabilities.FluidHandler.ITEM, + (stack, context) -> new EmptyBottleFluidHandler(stack), + Items.GLASS_BOTTLE); + } + + /** + * A glass bottle used on a bottleable source fills, both sides, before vanilla's bottle handling + * (which only knows water). Anything else goes on to vanilla. + */ + private static void fillFromWorld(PlayerInteractEvent.RightClickItem event) { + var held = event.getItemStack(); + if (!held.is(Items.GLASS_BOTTLE)) return; + var player = event.getEntity(); + var level = event.getLevel(); + var hit = player.pick(player.blockInteractionRange(), 0, true); + if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult block)) return; + var pos = block.getBlockPos(); + var fluid = level.getFluidState(pos); + if (!fluid.isSource() || !bottleable(fluid.getType()) || !level.mayInteract(player, pos)) return; + level.playSound( + player, + player.getX(), + player.getY(), + player.getZ(), + SoundEvents.BOTTLE_FILL, + SoundSource.NEUTRAL, + 1.0F, + 1.0F); + player.awardStat(Stats.ITEM_USED.get(Items.GLASS_BOTTLE)); + player.setItemInHand( + event.getHand(), + ItemUtils.createFilledResult(held, player, FluidBottleItem.filled(fluid.getType()))); + event.setCanceled(true); + event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide())); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/common/fluid/package-info.java b/src/main/java/zone/moddev/mc/lib/common/fluid/package-info.java new file mode 100644 index 00000000..39634ffb --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/common/fluid/package-info.java @@ -0,0 +1,5 @@ +/** + * Glass bottles that hold any fluid in the {@code #mmdlib:bottleable} tag; see {@link + * zone.moddev.mc.lib.common.fluid.FluidBottles}. + */ +package zone.moddev.mc.lib.common.fluid; diff --git a/src/main/resources/assets/mmdlib/lang/en_us.json b/src/main/resources/assets/mmdlib/lang/en_us.json index 1d4e8032..a1b8ffc5 100644 --- a/src/main/resources/assets/mmdlib/lang/en_us.json +++ b/src/main/resources/assets/mmdlib/lang/en_us.json @@ -377,5 +377,7 @@ "screen.mmdlib.stats.cycles": "Cycles", "screen.mmdlib.stats.cycle_tip": "Click to cycle tick / second / minute. Right-click to cycle backwards.", "screen.mmdlib.stats.empty": "No statistics available.", - "screen.mmdlib.stats.tip": "Measured RF: latest 20 ticks. Cycles: up to 60 game seconds. Max cycles uses the active recipe duration; max use is peak recipe demand. Max transfer rates use one transfer per tick." + "screen.mmdlib.stats.tip": "Measured RF: latest 20 ticks. Cycles: up to 60 game seconds. Max cycles uses the active recipe duration; max use is peak recipe demand. Max transfer rates use one transfer per tick.", + "item.mmdlib.fluid_bottle": "Fluid Bottle", + "item.mmdlib.fluid_bottle.filled": "Bottle of %s" } diff --git a/src/main/resources/assets/mmdlib/models/item/fluid_bottle.json b/src/main/resources/assets/mmdlib/models/item/fluid_bottle.json new file mode 100644 index 00000000..38ee9d85 --- /dev/null +++ b/src/main/resources/assets/mmdlib/models/item/fluid_bottle.json @@ -0,0 +1,11 @@ +{ + "parent": "neoforge:item/default", + "loader": "neoforge:fluid_container", + "fluid": "minecraft:empty", + "cover_is_mask": false, + "textures": { + "fluid": "minecraft:item/potion_overlay", + "cover": "minecraft:item/potion", + "particle": "minecraft:item/potion" + } +} diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/FluidBottleSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/FluidBottleSmoke.java new file mode 100644 index 00000000..e4216d98 --- /dev/null +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/FluidBottleSmoke.java @@ -0,0 +1,106 @@ +package zone.moddev.mc.lib.smoke; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; + +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.FluidUtil; +import net.neoforged.neoforge.fluids.capability.templates.FluidTank; + +import zone.moddev.mc.lib.common.fluid.FluidBottleItem; +import zone.moddev.mc.lib.common.fluid.FluidBottles; + +/** + * Glass bottles and {@code #mmdlib:bottleable}, which test resources fill with molten copper only. + */ +final class FluidBottleSmoke { + private FluidBottleSmoke() {} + + private static Fluid fluid(String id) { + return BuiltInRegistries.FLUID.get(ResourceLocation.parse(id)); + } + + /** A player standing above {@code source}, looking straight down at it, holding two bottles. */ + private static ServerPlayer above(GameTestHelper helper, BlockPos source) { + var player = helper.makeMockServerPlayerInLevel(); + player.setGameMode(GameType.SURVIVAL); + player.moveTo(source.getX() + .5, source.getY() + 1, source.getZ() + .5, 0, 90); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.GLASS_BOTTLE, 2)); + return player; + } + + /** A bottle used on molten copper fills; the source stays; untagged fluids are refused. */ + static void world(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var pos = helper.absolutePos(center); + var copper = fluid("mmdlib:copper_molten"); + level.setBlock(pos, copper.defaultFluidState().createLegacyBlock(), 3); + var player = above(helper, pos); + player.gameMode.useItem(player, level, player.getMainHandItem(), InteractionHand.MAIN_HAND); + require(player.getMainHandItem().is(Items.GLASS_BOTTLE) && player.getMainHandItem().getCount() == 1, + "one bottle used"); + var filled = player.getInventory().items.stream().filter(s -> s.is(FluidBottles.BOTTLE.get())).findFirst(); + require(filled.isPresent(), "a fluid bottle was given"); + var contents = FluidBottleItem.contents(filled.get()); + require(contents.is(copper) && contents.getAmount() == FluidBottles.CAPACITY, "it holds 333 mB of copper"); + require(level.getFluidState(pos).isSource(), "the source is left in place"); + + var iron = fluid("mmdlib:iron_molten"); + level.setBlock(pos, iron.defaultFluidState().createLegacyBlock(), 3); + var other = above(helper, pos); + other.gameMode.useItem(other, level, other.getMainHandItem(), InteractionHand.MAIN_HAND); + require(other.getMainHandItem().getCount() == 2, "untagged molten iron is not bottled"); + helper.succeed(); + } + + /** Water stays vanilla's: a water potion, not a fluid bottle. */ + static void water(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var pos = helper.absolutePos(center); + level.setBlock(pos, Blocks.WATER.defaultBlockState(), 3); + var player = above(helper, pos); + player.gameMode.useItem(player, level, player.getMainHandItem(), InteractionHand.MAIN_HAND); + require(player.getInventory().items.stream().anyMatch(s -> s.is(Items.POTION)), "vanilla made a water potion"); + require(player.getInventory().items.stream().noneMatch(s -> s.is(FluidBottles.BOTTLE.get())), "no fluid bottle"); + helper.succeed(); + } + + /** Tanks fill glass bottles with 333 mB of a bottleable fluid, and take it back. */ + static void tanks(GameTestHelper helper) { + var copper = fluid("mmdlib:copper_molten"); + var tank = new FluidTank(1000); + tank.fill(new FluidStack(copper, 1000), FluidTank.FluidAction.EXECUTE); + var filled = FluidUtil.tryFillContainer(new ItemStack(Items.GLASS_BOTTLE), tank, 1000, null, true); + require(filled.isSuccess() && filled.getResult().is(FluidBottles.BOTTLE.get()), "a tank fills a glass bottle"); + require(tank.getFluidAmount() == 1000 - FluidBottles.CAPACITY, "the tank gave 333 mB"); + + var iron = new FluidTank(1000); + iron.fill(new FluidStack(fluid("mmdlib:iron_molten"), 1000), FluidTank.FluidAction.EXECUTE); + require(!FluidUtil.tryFillContainer(new ItemStack(Items.GLASS_BOTTLE), iron, 1000, null, true).isSuccess(), + "untagged fluids are refused"); + var water = new FluidTank(1000); + water.fill(new FluidStack(Fluids.WATER, 1000), FluidTank.FluidAction.EXECUTE); + require(!FluidUtil.tryFillContainer(new ItemStack(Items.GLASS_BOTTLE), water, 1000, null, true).isSuccess(), + "water stays vanilla's"); + + var empty = new FluidTank(1000); + var poured = FluidUtil.tryEmptyContainer(filled.getResult(), empty, 1000, null, true); + require(poured.isSuccess() && poured.getResult().is(Items.GLASS_BOTTLE), "emptying leaves a glass bottle"); + require(empty.getFluidAmount() == FluidBottles.CAPACITY && empty.getFluid().is(copper), "333 mB poured"); + helper.succeed(); + } + + private static void require(boolean condition, String what) { + if (!condition) throw new AssertionError(what); + } +} diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java index 64fe4605..d904421e 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java @@ -129,6 +129,7 @@ public static void toolsAndArmor(GameTestHelper helper) throws Exception { } @GameTest(template = EMPTY) +<<<<<<< Updated upstream public static void itemUseHooks(GameTestHelper helper) { HookSmoke.use(helper); } @@ -166,6 +167,20 @@ public static void scriptBlockHooks(GameTestHelper helper) { @GameTest(template = EMPTY) public static void redstoneGate(GameTestHelper helper) { RedstoneSmoke.gate(helper, CENTER); +======= + public static void fluidBottlesFromWorld(GameTestHelper helper) { + FluidBottleSmoke.world(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void fluidBottlesLeaveWaterToVanilla(GameTestHelper helper) { + FluidBottleSmoke.water(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void fluidBottlesAndTanks(GameTestHelper helper) { + FluidBottleSmoke.tanks(helper); +>>>>>>> Stashed changes } @GameTest(template = EMPTY) diff --git a/src/runtimeTest/resources/data/mmdlib/tags/fluid/bottleable.json b/src/runtimeTest/resources/data/mmdlib/tags/fluid/bottleable.json new file mode 100644 index 00000000..51f098df --- /dev/null +++ b/src/runtimeTest/resources/data/mmdlib/tags/fluid/bottleable.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["mmdlib:copper_molten"] +} From de5acbb7f0fccd83fd84445baa2fe1d3626845e4 Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 17:37:24 -0500 Subject: [PATCH 2/2] Resolve leftover conflict markers from the fluid bottle stash 10cf121 was committed with unresolved stash conflicts in MMDLib.java and MMDLibGameTests.java, so the branch did not compile. Keep both sides: the fluid bottle registration next to the CraftTweaker one, and the fluid bottle game tests next to the hook and redstone tests. Co-Authored-By: Claude Opus 5.5 --- src/main/java/zone/moddev/mc/lib/MMDLib.java | 5 +---- .../java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/zone/moddev/mc/lib/MMDLib.java b/src/main/java/zone/moddev/mc/lib/MMDLib.java index 34831328..cc44dc59 100644 --- a/src/main/java/zone/moddev/mc/lib/MMDLib.java +++ b/src/main/java/zone/moddev/mc/lib/MMDLib.java @@ -117,13 +117,10 @@ public MMDLib(IEventBus modBus, ModContainer container) { NeoForge.EVENT_BUS.addListener( zone.moddev.mc.lib.machine.neoforge.MachinePartsLoader::register); modBus.addListener(zone.moddev.mc.lib.util.Registries::register); -<<<<<<< Updated upstream + zone.moddev.mc.lib.common.fluid.FluidBottles.register(modBus); if (ModList.get().isLoaded("crafttweaker")) CraftTweakerInit.register(modBus); -======= - zone.moddev.mc.lib.common.fluid.FluidBottles.register(modBus); ->>>>>>> Stashed changes modBus.addListener(this::publishDefinitions); modBus.addListener((FMLConstructModEvent event) -> event.enqueueWork(this::loadDefinitions)); new ItemEventBridge( diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java index d904421e..57daab5e 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java @@ -129,7 +129,6 @@ public static void toolsAndArmor(GameTestHelper helper) throws Exception { } @GameTest(template = EMPTY) -<<<<<<< Updated upstream public static void itemUseHooks(GameTestHelper helper) { HookSmoke.use(helper); } @@ -167,7 +166,9 @@ public static void scriptBlockHooks(GameTestHelper helper) { @GameTest(template = EMPTY) public static void redstoneGate(GameTestHelper helper) { RedstoneSmoke.gate(helper, CENTER); -======= + } + + @GameTest(template = EMPTY) public static void fluidBottlesFromWorld(GameTestHelper helper) { FluidBottleSmoke.world(helper, CENTER); } @@ -180,7 +181,6 @@ public static void fluidBottlesLeaveWaterToVanilla(GameTestHelper helper) { @GameTest(template = EMPTY) public static void fluidBottlesAndTanks(GameTestHelper helper) { FluidBottleSmoke.tanks(helper); ->>>>>>> Stashed changes } @GameTest(template = EMPTY)