User Tools

Site Tools






Blocks and Block Entities



World Generation

Recipe Types




Dynamic Data Generation

Tutorials for Minecraft 1.15

Tutorials for Minecraft 1.14

Contribute to Fabric

Extremely Strange People


Fabric Transfer API: Understanding Storage<FluidVariant>

This article is part of a series on the Fabric Transfer API. Link to the home page of the series.

Storage<FluidVariant> is a thing that contains fluids, it is what tanks, buckets, etc… implement and it is what pipe mods and others use to move fluids between containers.

You already used Storage<FluidVariant>

Now that you implemented a simple tank, let's have a look at what exactly this SingleVariantStorage<FluidVariant> is.

public abstract class SingleVariantStorage<T extends TransferVariant<?>> extends ... implements SingleSlotStorage<T> { ... }
public interface SingleSlotStorage<T> extends Storage<T>, ... { ... }

The takeaway here is that SingleVariantStorage<FluidVariant> is a Storage<FluidVariant>, which is what we registered to FluidStorage.SIDED!

Retrieving a Storage<FluidVariant> from the world

Let's see how we can retrieve one from the world:

World world = ...; // Important: must be a server world!!! You must never query this on the client.
BlockPos pos = ...; // The postion in the world
Direction direction = ...; // The side for which we want a storage.
Storage<FluidVariant> storage = FluidStorage.SIDED.find(world, pos, direction);
if (storage != null) {
	// Use the storage

A look at Storage<T>

Let's have a look at

public interface Storage<T> {
	// Try to insert a resource in the storage, return how much was inserted.
	long insert(T resource, long maxAmount, TransactionContext transaction);
	// Try to extract a resource from the storage, return how much was extracted.
	long extract(T resource, long maxAmount, TransactionContext transaction);
	// Iterate over the contents of this storage.
	default Iterable<StorageView<T>> iterable(TransactionContext transaction) { ... }

This interface allows us to insert into a storage, extract from it, and read its contents.

First example: how to insert exactly one bucket of water into a storage

Storage<FluidVariant> storage = ...;
FluidVariant water = FluidVariant.of(Fluids.WATER);
// Open a transaction: this allows cancelling the operation if it doesn't go as expected.
try (Transaction transaction = Transaction.openOuter()) {
	// Try to insert, will return how much was actually inserted.
	long amountInserted = storage.insert(water, FluidConstants.BUCKET, transaction);
	if (amountInserted == FluidConstants.BUCKET) {
		// "Commit" the transaction: this validates all the operations that were part of this transaction.
		// You should call this if you are satisfied with the result of the operation, and want to keep it.
	} else {
		// Doing nothing "aborts" the transaction: this cancels the insertion.
		// You should call this if you are not satisfied with the result of the operation, and want to abort it.

Second example: move exactly one bucket of lava from a storage to another storage

import static net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants.BUCKET; // a bit shorter to type :)
Storage<FluidVariant> source, destination;
FluidVariant lava = FluidVariant.of(Fluids.LAVA);
try (Transaction transaction = Transaction.openOuter()) {
	if (source.extract(lava, BUCKET, transaction) == BUCKET && destination.insert(lava, BUCKET, transaction) == BUCKET) {

Hopefully you understand why this works, and why this will never duplicate or void fluid.

A more complicated example: Extracting the contents of a storage

In the previous example, we knew that we wanted to move water or lava. But what if we don't know what to extract? The answer is: iterate over the contents! This example also introduces the concept of nested transactions.

Storage<FluidVariant> storage;
Predicate<FluidVariant> filter = fv -> fv.isOf(Fluids.WATER); // What we want to extract, in this example let's say we want to extract any water, regardless of its NBT tag.
long totalExtracted = 0;
// Open a transaction, as before.
try (Transaction transaction = Transaction.openOuter()) {
	// Loop over the contents! Each StorageView<T> can contain at most one resource. You can think of a StorageView as an inventory slot.
	for (StorageView<FluidVariant> view : storage.iterable(transaction)) {
		if (view.isResourceBlank()) continue; // This means that the view contains no resource, represented by FluidVariant.blank().
		FluidVariant storedResource = view.getResource(); // Current resource
		if (!filter.test(storedResource)) continue; // The filter rejected this resource, skip it.
		// If you want to extract any amount <= view.getAmount(), do this.
		totalExtracted += view.extract(storedResource, view.getAmount(), transaction);
		// If you want to extract either the exact amount or nothing, you can use a nested transaction!
		try (Transaction nestedTransaction = transaction.openNested()) {
			long amount = view.getAmount(); // The amount will change after the call to extract, so make sure to save it first.
			long extracted = view.extract(storedResource, amount, nestedTransaction);
			if (extracted == amount) {
				totalExtracted += amount;
				nestedTransaction.commit(); // Validate the nestedTransaction: the outer one will have to committed as well to validate this change.
			} else {
				// If we do nothing, the extraction is cancelled immediately when nestedTransaction is closed at the end of the try { ... } block.
	transaction.commit(); // Don't forget to commit, or all of the extraction will be cancelled!


You should now be able to use Storages and StorageViews. They can be a bit challenging in the beginning, but once you're used to them they are very easy to work with.

You should also have a look at StorageUtil: it already contains functions that perform common transfer operations. In particular, move is very useful if you just want to move resources between two storages.

tutorial/transfer-api_storage.txt · Last modified: 2021/10/30 10:25 by technici4n