====== Fabric Transfer API: Fluid-containing items ======
//This article is part of a series on the Fabric Transfer API. [[tutorial: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.
===== The problem =====
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:
* The first step is to remove 1 water bucket item from the current slot, that is the slot that contains the water bucket.
* The second step is to try to add one empty bucket item to the current slot, at the same position.
* If that fails, the third step is to add the empty bucket item somewhere else in the inventory.
* The water extraction can only proceed if both step 1, and step 2 or 3, succeed.
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 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 =====
[[https://github.com/FabricMC/fabric/blob/1.18/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java|ContainerItemContext]] represents the inventory containing the fluid container, and it is made of the following parts:
* The specific slot in the inventory that the fluid container is queried from. In the example above, this is the slot containing the water bucket, used for steps 1 and 2.
* An overflow insertion function that can be used to insert items into the context's inventory when insertion into the main slot fails. In our example above, this is the function used for step 3.
* The context may also contain additional slots.
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.
==== Obtaining instances ====
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.
==== The API in action ====
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 handStorage = handContext.find(FluidStorage.ITEM);
if (handStorage != null) {
// Use the storage: any usual Storage can be attempted.
try (Transaction transaction = Transaction.openOuter()) {
handStorage.insert(FluidVariant.of(Fluids.WATER), FluidConstants.BUCKET, transaction);
transaction.commit();
}
}
TODO:
- filling an item, example from TR
- using the existing base implementations for items