Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions docs/fluid-bottles.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ 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.

## Experience fluids

Fluids in NeoForge's `#c:experience` tag, from whatever mod adds them, are a
special case: a glass bottle takes **250 mB** (the tag's convention) and becomes
vanilla's **Bottle o' Enchanting**, not an MMDLib bottle. They don't need the
`#mmdlib:bottleable` tag. Right-clicking a source and filling from tanks both
work, as above. It is one way only: a Bottle o' Enchanting can't be poured back
into a tank, its XP comes back by throwing it.

## What works

* **Right-click a source block** with a glass bottle to fill it. The source block
Expand All @@ -43,8 +52,11 @@ the usual priority are seen first.
`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.
`FluidBottleItem.contents(stack)` reads one. `FluidBottles.capacity(fluid)` is
the single rule for what can be bottled and how much a bottle takes (0 for
neither), built on `bottleable(fluid)` and `experience(fluid)`;
`FluidBottles.filled(fluid)` gives what a full glass bottle becomes.

`FluidBottleSmoke` (game tests) checks filling from the world, water left to
vanilla, untagged fluids refused, and filling and emptying through tanks.
vanilla, untagged fluids refused, filling and emptying through tanks, and
experience fluids becoming Bottles o' Enchanting (molten gold stands in for one).
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
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
* The fluid capability of an empty glass bottle: it takes exactly {@link FluidBottles#capacity} of
* a bottleable or experience fluid at once and becomes {@link FluidBottles#filled}, 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 {
Expand All @@ -35,22 +35,22 @@ public FluidStack getFluidInTank(int tank) {

@Override
public int getTankCapacity(int tank) {
return FluidBottles.CAPACITY;
// Not knowing the fluid, the larger of the two: experience bottles take less.
return Math.max(FluidBottles.CAPACITY, FluidBottles.EXPERIENCE_CAPACITY);
}

@Override
public boolean isFluidValid(int tank, FluidStack stack) {
return FluidBottles.bottleable(stack.getFluid());
return FluidBottles.capacity(stack.getFluid()) > 0;
}

@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;
var capacity = FluidBottles.capacity(resource.getFluid());
if (container.getCount() != 1 || capacity == 0 || resource.getAmount() < capacity) return 0;
if (action.execute()) container = FluidBottles.filled(resource.getFluid());
return capacity;
}

@Override
Expand Down
40 changes: 36 additions & 4 deletions src/main/java/zone/moddev/mc/lib/common/fluid/FluidBottles.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import net.minecraft.stats.Stats;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemUtils;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.material.Fluid;
Expand All @@ -26,6 +27,7 @@
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.common.Tags;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.fluids.SimpleFluidContent;
import net.neoforged.neoforge.fluids.capability.templates.FluidHandlerItemStack;
Expand All @@ -46,6 +48,11 @@
* </ul>
*
* Vanilla's own fluids (water, lava) are always left to vanilla, tagged or not.
*
* <p>Experience fluids ({@code #c:experience}, whatever mod adds them) are a special case: a glass
* bottle takes {@link #EXPERIENCE_CAPACITY} of one and becomes vanilla's Bottle o' Enchanting, no
* {@link #BOTTLEABLE} tag needed. That is one way only: the XP comes back out of a thrown bottle,
* not out of the item.
*/
public final class FluidBottles {
private static final Logger LOGGER = LogUtils.getLogger();
Expand All @@ -57,6 +64,12 @@ public final class FluidBottles {
/** Millibuckets in a bottle: a third of a bucket, the common convention. */
public static final int CAPACITY = 333;

/**
* Millibuckets of experience fluid in a Bottle o' Enchanting: the {@link Tags.Fluids#EXPERIENCE}
* convention.
*/
public static final int EXPERIENCE_CAPACITY = 250;

private static final DeferredRegister.DataComponents COMPONENTS =
DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MMDLib.MODID);

Expand Down Expand Up @@ -90,6 +103,25 @@ public static boolean bottleable(Fluid fluid) {
&& fluid.defaultFluidState().is(BOTTLEABLE);
}

/** Whether {@code fluid} is an experience fluid, which bottles into a Bottle o' Enchanting. */
public static boolean experience(Fluid fluid) {
return fluid.defaultFluidState().is(Tags.Fluids.EXPERIENCE);
}

/** Millibuckets a glass bottle takes of {@code fluid}, or 0 if it can't hold it. */
public static int capacity(Fluid fluid) {
if (experience(fluid)) return EXPERIENCE_CAPACITY;
return bottleable(fluid) ? CAPACITY : 0;
}

/**
* What a glass bottle full of {@code fluid} becomes: a Bottle o' Enchanting for experience, a
* {@link FluidBottleItem} otherwise. Only meaningful when {@link #capacity} is non-zero.
*/
public static ItemStack filled(Fluid fluid) {
return experience(fluid) ? new ItemStack(Items.EXPERIENCE_BOTTLE) : FluidBottleItem.filled(fluid);
}

private static void capabilities(RegisterCapabilitiesEvent event) {
event.registerItem(
Capabilities.FluidHandler.ITEM,
Expand All @@ -98,7 +130,7 @@ private static void capabilities(RegisterCapabilitiesEvent event) {
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());
return bottleable(fluid.getFluid()) && !experience(fluid.getFluid());
}
},
BOTTLE.get());
Expand All @@ -115,7 +147,7 @@ public boolean canFillFluidType(net.neoforged.neoforge.fluids.FluidStack fluid)
}

/**
* A glass bottle used on a bottleable source fills, both sides, before vanilla's bottle handling
* A glass bottle used on a bottleable or experience 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) {
Expand All @@ -127,7 +159,7 @@ private static void fillFromWorld(PlayerInteractEvent.RightClickItem event) {
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;
if (!fluid.isSource() || capacity(fluid.getType()) == 0 || !level.mayInteract(player, pos)) return;
level.playSound(
player,
player.getX(),
Expand All @@ -140,7 +172,7 @@ private static void fillFromWorld(PlayerInteractEvent.RightClickItem event) {
player.awardStat(Stats.ITEM_USED.get(Items.GLASS_BOTTLE));
player.setItemInHand(
event.getHand(),
ItemUtils.createFilledResult(held, player, FluidBottleItem.filled(fluid.getType())));
ItemUtils.createFilledResult(held, player, filled(fluid.getType())));
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Glass bottles that hold any fluid in the {@code #mmdlib:bottleable} tag; see {@link
* Glass bottles that hold any fluid in the {@code #mmdlib:bottleable} tag, and experience fluids that bottle into Bottles o' Enchanting; see {@link
* zone.moddev.mc.lib.common.fluid.FluidBottles}.
*/
package zone.moddev.mc.lib.common.fluid;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import zone.moddev.mc.lib.common.fluid.FluidBottles;

/**
* Glass bottles and {@code #mmdlib:bottleable}, which test resources fill with molten copper only.
* Glass bottles and {@code #mmdlib:bottleable}, which test resources fill with molten copper only,
* and {@code #c:experience}, which they fill with molten gold.
*/
final class FluidBottleSmoke {
private FluidBottleSmoke() {}
Expand Down Expand Up @@ -100,6 +101,42 @@ static void tanks(GameTestHelper helper) {
helper.succeed();
}

/** A bottle used on an experience source becomes a Bottle o' Enchanting; the source stays. */
static void experienceWorld(GameTestHelper helper, BlockPos center) {
var level = helper.getLevel();
var pos = helper.absolutePos(center);
level.setBlock(pos, fluid("mmdlib:gold_molten").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");
require(player.getInventory().items.stream().anyMatch(s -> s.is(Items.EXPERIENCE_BOTTLE)),
"a Bottle o' Enchanting was given");
require(player.getInventory().items.stream().noneMatch(s -> s.is(FluidBottles.BOTTLE.get())), "no fluid bottle");
require(level.getFluidState(pos).isSource(), "the source is left in place");
helper.succeed();
}

/** Tanks of experience fill glass bottles 250 mB at a time; the result can't be poured back. */
static void experienceTanks(GameTestHelper helper) {
var gold = fluid("mmdlib:gold_molten");
var tank = new FluidTank(1000);
tank.fill(new FluidStack(gold, 1000), FluidTank.FluidAction.EXECUTE);
var filled = FluidUtil.tryFillContainer(new ItemStack(Items.GLASS_BOTTLE), tank, 1000, null, true);
require(filled.isSuccess() && filled.getResult().is(Items.EXPERIENCE_BOTTLE), "a tank makes a Bottle o' Enchanting");
require(tank.getFluidAmount() == 1000 - FluidBottles.EXPERIENCE_CAPACITY, "the tank gave 250 mB");

var low = new FluidTank(1000);
low.fill(new FluidStack(gold, FluidBottles.EXPERIENCE_CAPACITY - 1), FluidTank.FluidAction.EXECUTE);
require(!FluidUtil.tryFillContainer(new ItemStack(Items.GLASS_BOTTLE), low, 1000, null, true).isSuccess(),
"less than a bottle's worth is refused");

var empty = new FluidTank(1000);
require(!FluidUtil.tryEmptyContainer(filled.getResult(), empty, 1000, null, true).isSuccess(),
"a Bottle o' Enchanting does not pour back");
helper.succeed();
}

private static void require(boolean condition, String what) {
if (!condition) throw new AssertionError(what);
}
Expand Down
10 changes: 10 additions & 0 deletions src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ public static void fluidBottlesAndTanks(GameTestHelper helper) {
FluidBottleSmoke.tanks(helper);
}

@GameTest(template = EMPTY)
public static void experienceBottlesFromWorld(GameTestHelper helper) {
FluidBottleSmoke.experienceWorld(helper, CENTER);
}

@GameTest(template = EMPTY)
public static void experienceBottlesFromTanks(GameTestHelper helper) {
FluidBottleSmoke.experienceTanks(helper);
}

@GameTest(template = EMPTY)
public static void capabilities(GameTestHelper helper) {
CapabilitySmoke.check(helper.getLevel());
Expand Down
4 changes: 4 additions & 0 deletions src/runtimeTest/resources/data/c/tags/fluid/experience.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"replace": false,
"values": ["mmdlib:gold_molten"]
}
Loading