====== Fabric Transfer API: How to implement Storage ====== //This article is part of a series on the Fabric Transfer API. [[tutorial:transfer-api|Link to the home page of the series]].// Now that you understand how to find instances of ''Storage'' and how to use them, you are now ready to learn how it can be implemented. It's possible to implement directly by implementing the interface and filling the methods, but Fabric API already provides many implementations that you can use and combine for almost any task. ===== Overview of the base implementations ===== ==== SingleVariantStorage ==== [[https://github.com/FabricMC/fabric/blob/1.18/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java|SingleVariantStorage]] is a storage implementation that can only store a single fluid at a given time. It's suitable for a single tank or "fluid slot". Usage example: public final SingleVariantStorage fluidStorage = new SingleVariantStorage<>() { @Override protected FluidVariant getBlankVariant() { return FluidVariant.blank(); // This should always be FluidVariant.blank() for fluid storages. } @Override protected long getCapacity(FluidVariant variant) { // Here, you can pick your capacity depending on the fluid variant. // For example, if we want to store 8 buckets of any fluid: return 8 * FluidConstants.BUCKET; } @Override protected void onFinalCommit() { // Optional: anything that needs to be done after a successful insertion or extraction. Calling markDirty at the very least is recommended. } @Override protected boolean canInsert(FluidVariant variant) { // Optional: can be used to prevent insertion of some fluid variants. } @Override protected boolean canExtract(FluidVariant variant) { // Optional: can be used to prevent extraction of some fluid variants. } }; ==== FilteringStorage ==== [[https://github.com/FabricMC/fabric/blob/1.18/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/FilteringStorage.java|FilteringStorage]] can filter access to another existing storage. ''insertOnlyOf'' and ''extractOnlyOf'' can be useful to create a new object that can only insert or extract into an existing storage. Usage example: // Storage that we already have, allows any insertion or extraction. Storage existingStorage = ...; // Wrapper that will forward any insertion to existingStorage, but will return 0 for any extraction attempt. Storage insertionWrapper = FilteringStorage.insertOnlyOf(existingStorage); // Wrapper that will forward any extraction to existingStorage, but will return 0 for any insertion attempt. Storage extractionWrapper = FilteringStorage.extractOnlyOf(existingStorage); Subclassing ''FilteringStorage'' and overriding ''canInsert'' and/or ''canExtract'' is also possible if more advanced filtering is necessary. ==== CombinedStorage ==== [[https://github.com/FabricMC/fabric/blob/1.18/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedStorage.java|CombinedStorage]] can create a ''Storage'' that wraps multiple storages. Usage example: Storage storage1, storage2, storage3 = ...; // This combined storage will insert and extract from 1, 2, and 3 (in that order). Storage combinedStorage = new CombinedStorage<>(List.of(storage1, storage2, storage3)); ===== A more involved example ===== To better understand how these classes can be combined together, let's write a machine that converts water to lava. Here are our requirements: * The machine will have two internal "tanks" that can store 4 buckets each: one tank that contains water, and one that contains lava. * Each tick, the machine will convert as much water as possible into steam. * External pipes will only be allowed to insert into the water tank (no extraction), and they will only be allowed to extract from the lava tank (no insertion). * The water tank will only be accessible from the top and the sides of the machine. The lava tank will only be accessible from the bottom and the sides of the machine. In other words: top can only access water, bottom can only access lava, and sides can access both. For the internal tanks, we can just use ''SingleVariantStorage''. Note that if we override ''canExtract'' for the water tank to prevent extraction, we won't be able to extract water to convert it to water. What we will do instead is use ''FilteringStorage'' to prevent external pipes from extracting water or inserting lava. Finally, we will use ''CombinedStorage'' so that both tanks can be accessed from the sides. ==== Internal tanks ==== First, let's add the internal tanks to the block entity: import static net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants.BUCKET; public class MachineBlockEntity extends BlockEntity { // Useful constants. private static final FluidVariant WATER = FluidVariant.of(Fluids.WATER); private static final FluidVariant LAVA = FluidVariant.of(Fluids.LAVA); // A helper function to create a single tank that can store 4 buckets, and is restricted to a single variant. private SingleVariantStorage createTank(FluidVariant allowedVariant) { return new SingleVariantStorage<>() { @Override protected FluidVariant getBlankVariant() { return FluidVariant.blank(); } @Override protected long getCapacity(FluidVariant variant) { return 4 * BUCKET; } @Override protected boolean canInsert(FluidVariant variant) { // Only allow the specified variant. return variant.equals(allowedVariant); } @Override protected void onFinalCommit() { markDirty(); } }; } // Our two internal tanks. private final SingleVariantStorage waterTank = createTank(WATER); private final SingleVariantStorage lavaTank = createTank(LAVA); // Don't forget to implement toNbt and fromNbt to save the data of the tanks. // ... [other code omitted] } ==== Conversion logic ==== The conversion logic is straightforward: public class MachineBlockEntity extends BlockEntity { // ... [other code] // Call this on server tick: public void doConversion() { try (Transaction transaction = Transaction.openOuter()) { long lavaInserted = lavaTank.insert(LAVA, waterTank.amount, transaction); waterTank.extract(WATER, lavaInserted, transaction); transaction.commit(); } } } ==== Exposing the tanks to external pipes ==== We can use the filtering and combined storages to ensure that pipes can only do what we want them to do: public class MachineBlockEntity extends BlockEntity { // ... [other code] // Wrapper around the internal tanks that restrict operations to what we want to allow. public final Storage exposedWaterTank = FilteringStorage.insertOnlyOf(waterTank); public final Storage exposedLavaTank = FilteringStorage.extractOnlyOf(lavaTank); // Wrapper around both, for side access public final Storage exposedTanks = new CombinedStorage<>(List.of(exposedWaterTank, exposedLavaTank)); } Finally, let's not forget to register our block entity to ''FluidStorage.SIDED'': BlockEntityType MACHINE = null; FluidStorage.SIDED.registerForBlockEntity((machine, direction) -> switch (direction) { // Only expose the water tank from the top side. case UP -> machine.exposedWaterTank; // Only expose the lava tank from the bottom side. case DOWN -> machine.exposedLavaTank; // Expose both otherwise (access from one of the 4 sides). default -> machine.exposedTanks; }, MACHINE);