This article is part of a series on the Fabric Transfer API. Link to the home page of the series.
This tutorial focuses on “fluid-containing items”, i.e. items such as buckets or tanks. This is a complex topic, so make sure you read the previous fluid and item tutorials first.
When we are dealing with fluid-containing items, we are interacting with fluid containers stored inside of an inventory. For example, this is the sequence of operations that must be executed to empty a water bucket:
You do not need to understand this in detail, but this should give an idea of where we are headed. In code, this is what this looks like: (taken from FullItemFluidStorage
with comments adjusted)
// This is the important field: the "context" represents the underlying inventory - more on that in a bit. private final ContainerItemContext context; // A few constants, ignore these for now private final Item fullItem = Items.WATER_BUCKET; private final Function<ItemVariant, ItemVariant> fullToEmptyMapping = fullBucket -> ItemVariant.of(Items.BUCKET, fullBucket.getNbt()); // This preserves NBT, such as the custom name of a bucket. private final FluidVariant containedFluid = FluidVariant.of(Fluids.WATER); private final long containedAmount = FluidConstants.BUCKET; @Override public long extract(FluidVariant resource, long maxAmount, TransactionContext transaction) { StoragePreconditions.notBlankNotNegative(resource, maxAmount); // Defensive check, this is good practice. // If the context's item is not a bucket anymore, can't extract! if (!context.getItemVariant().isOf(fullItem)) return 0; // Make sure that the fluid and the amount match. if (resource.equals(containedFluid) && maxAmount >= containedAmount) { // If that's ok, just convert one of the full item into the empty item, copying the nbt. ItemVariant newVariant = fullToEmptyMapping.apply(context.getItemVariant()); // Exchange removes 1 full bucket, and adds 1 empty bucket. if (context.exchange(newVariant, 1, transaction) == 1) { // Conversion ok! return containedAmount; } } return 0; }
ContainerItemContext represents the inventory containing the fluid container, and it is made of the following parts:
You usually don't interact with these methods directly, since ContainerItemContext
has many useful default-implemented methods. Make sure to read the javadoc if you ever need to implement or use it.
Fabric provides various static methods to create a ContainerItemContext
, depending on your use case. The most important ones are the following:
ContainerItemContext.ofPlayerHand(player, hand)
creates a context for the slot of the passed hand, and with any overflow sent back to the player.ContainerItemContext.ofPlayerCursor(player, screenHandler)
creates a context for the cursor slot of the passed screen handler, and with any overflow sent back to the player.
A word of caution: don't use ContainerItemContext.withInitial(stack)
unless you know what you're doing. It does not mutate the stack.
An example to understand what is going on: how to query a storage for the main hand of a player, and insert 1 bucket of water into it:
PlayerEntity player; // Build the ContainerItemContext. ContainerItemContext handContext = ContainerItemContext.ofPlayerHand(player, Hand.MAIN_HAND); // Use it to query a fluid storage. Storage<FluidVariant> handStorage = handContext.find(FluidStorage.ITEM); if (handStorage != null) { // Use the storage: any usual Storage<FluidVariant> can be attempted. try (Transaction transaction = Transaction.openOuter()) { handStorage.insert(FluidVariant.of(Fluids.WATER), FluidConstants.BUCKET, transaction); transaction.commit(); } }
TODO: