User Tools

Site Tools


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