Fabric Transfer API: "fluid only" edition (#1356)

* Original fluid API design

* Rework the transaction system

* First javadoc pass

* Add a testmod, a base implementation for fluid storages and fix some little bugs

* Fix checkstyle

* Make Movement#move extract from the view and not the whole Storage

* Document and update FluidPreconditions

* Use for-each in CombinedStorage and document a little

* Remove useless overrides in Insertion/ExtractionOnlyStorage

* Move SnapshotParticipant#snapshots to the top of the class, and make updateSnapshots public

* Fix garbage collection of unused CauldronWrappers

* Use ArrayList directly

* Remove locking, reorganize transaction implementation, and add outer close callback

* Add more javadoc

* Rework Storage#forEach into Storage#iterator

* Add a few missing `transaction.addCloseCallback(iterator)`

* Add anyView(), exactView(), capacity() and isEmpty()

* Add Storage#iterable to make iteration friendlier to for loops

* Storages may now have multiple open iterators

Co-authored-by: Devan-Kerman <dev.sel20@gmail.com>

* Make CombinedStorage#supportsInsertion/Extraction iterate through the parts

* Block updates should be used when the supportsInsertion/Extraction status changes

* Fluid -> FluidKey

* Remove all references to ItemKey inside FluidKey, and other minor tweaks

* Cache FluidKeys with a null tag inside Fluid directly

* Fluid unit convention

* Add FluidKeyRendering and RenderHandler

* Bump version for more testing (also published to my maven)

* Add SingleViewIterator, massively reduce code duplication!

* Make API experimental, and add README

* Bump version

* Apparently Fluids.EMPTY is flowing

* Add package info

* Minor adjustements

* 1.17 port, cauldron support, add ResourceKey

* Checkstyle, gas rendering, use record for ResourceAmount

* Add a few helpers, rename some stuff

* Remove anyView, allow nullable in StorageUtil#find*, fix missing try block

* Slight findStoredResource cleanup

* Slightly improve implementation

* Bump version

* Fix wrong transaction

* I wrote in a comment that this could happen...

* Fix SingleFluidStorage bugs, add tests in the testmod, add testmod assets

* Add extract stick

* Rename a few things

* `ResourceKey<T>` -> `TransferKey<O>`
* `ResourceKey#getResource()` -> `TransferKey#getObject()` as resource is already widely used through the API for the keys themselves.
* `tag` -> `nbt`
* Add `get` prefixes to `StorageView` functions

* Bump version

* FluidKey -> FluidVariant

* Bump version

* Expand getVersion() documentation, make it thread-safe and use long.

Co-authored-by: Player <player@player.to>

* empty resource -> blank resource, and update SingleFluidStorage

Co-authored-by: Player <player@player.to>

* Make CauldronFluidContent a final class instead of a record.

Co-authored-by: Player <player@player.to>

* Get rid of CauldronFluidContent#minLevel (was always 1)

* Fix nested commits. (Thanks @warjort!)

* Separate Transaction and TransactionContext

Co-authored-by: Devan-Kerman <dev.sel20@gmail.com>
Co-authored-by: Player <player@player.to>

* Change WorldLocation into a private record

* Bump version

* Guard against exceptions thrown in close callbacks

* Make sure blank fluid variants don't have a tag

* Add documentation, make CauldronStorage clearer

Co-authored-by: frqnny <45723631+frqnny@users.noreply.github.com>

* Allow null storages in StorageUtil#move, and clarify sidedness of FluidStorage

* Add explicit hashCode and equals for transfer variants

* Remove ugly equals and hashCode overrides, and add constant time hashcode spec

Co-authored-by: Devan-Kerman <dev.sel20@gmail.com>
Co-authored-by: liach <liach@users.noreply.github.com>
Co-authored-by: Player <player@player.to>
Co-authored-by: frqnny <45723631+frqnny@users.noreply.github.com>
This commit is contained in:
Technici4n 2021-07-12 19:28:33 +02:00 committed by GitHub
parent 5f02c96920
commit c09be4c48a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 3602 additions and 1 deletions

View file

@ -175,7 +175,7 @@ allprojects {
checkstyle {
configFile = rootProject.file("checkstyle.xml")
toolVersion = "8.31"
toolVersion = "8.43"
}
tasks.withType(AbstractArchiveTask) {

View file

@ -0,0 +1,36 @@
# Fabric Transfer API (v1)
This module provides common facilities for the transfer of fluids and other game resources.
## Transactions
The [`Transaction`](src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/Transaction.java) system provides a
scope that can be used to simulate any number of transfer operations, and then cancel or validate all of them at once.
One can think of transactions as video game checkpoints. A more detailed explanation can be found in the class javadoc of `Transaction`.
Every transfer operation requires a `Transaction` parameter.
[`SnapshotParticipant`](src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/base/SnapshotParticipant.java)
is the reference implementation of a "participant", that is an object participating in a transaction.
## Storages
A [`Storage<T>`](src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java) is any object that can store resources of type `T`.
Its contents can be read, and resources can be inserted into it or extracted from it.
[`StorageUtil`](src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java) provides a few helpful functions to work with `Storage`s,
for example to move resources between two `Storage`s.
The [`storage/base`](src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base) package provides a few helpers to accelerate
implementation of `Storage<T>`.
## Fluid transfer
A `Storage<FluidVariant>` is any object that can store fluids. It is just a `Storage<T>`, where `T` is
[`FluidVariant`](src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidVariant.java), the immutable combination of a `Fluid` and additional NBT data.
Instances can be accessed through the API lookups defined in [`FluidStorage`](src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java).
Implementors of fluid inventories with a fixed number of "slots" or "tanks" can use
[`SingleFluidStorage`](src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java),
and combine them with `CombinedStorage`.
The unit for fluid transfer is 1/81000ths of a bucket, also known as _droplets_.
[`FluidConstants`](src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidConstants.java) contains a few helpful constants
to work with droplets.
Client-side [Fluid variant rendering](src/main/java/net/fabricmc/fabric/api/transfer/v1/client/fluid/FluidVariantRendering.java) will use regular fluid rendering by default,
ignoring the additional NBT data.
`Fluid`s that wish to render differently depending on the stored NBT data can register a
[`FluidVariantRenderHandler`](src/main/java/net/fabricmc/fabric/api/transfer/v1/client/fluid/FluidVariantRenderHandler.java).

View file

@ -0,0 +1,17 @@
archivesBaseName = "fabric-transfer-api-v1"
version = getSubprojectVersion(project, "0.3.4")
moduleDependencies(project, [
'fabric-api-base',
'fabric-api-lookup-api-v1',
'fabric-lifecycle-events-v1', // transitive dependency of API Lookup
'fabric-rendering-fluids-v1',
'fabric-textures-v0' // transitive dependency of Rendering Fluids
])
dependencies {
testmodImplementation project(path: ':fabric-object-builder-api-v1', configuration: 'dev')
testmodImplementation project(path: ':fabric-resource-loader-v0', configuration: 'dev')
testmodImplementation project(path: ':fabric-tag-extensions-v0', configuration: 'dev')
testmodImplementation project(path: ':fabric-tool-attribute-api-v1', configuration: 'dev')
}

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.client.fluid;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.item.TooltipContext;
import net.minecraft.client.texture.Sprite;
import net.minecraft.text.Text;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandler;
import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
/**
* Defines how {@linkplain FluidVariant fluid variants} of a given Fluid should be displayed to clients.
* Register with {@link FluidVariantRendering#register}.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
@Environment(EnvType.CLIENT)
public interface FluidVariantRenderHandler {
/**
* Return the name that should be used for the passed fluid variant.
*/
default Text getName(FluidVariant fluidVariant) {
return fluidVariant.getFluid().getDefaultState().getBlockState().getBlock().getName();
}
/**
* Append additional tooltips to the passed list if additional information is contained in the fluid variant.
*
* <p>The name of the fluid, and its identifier if the tooltip context is advanced, should not be appended.
* They are already added by {@link FluidVariantRendering#getTooltip}.
*/
default void appendTooltip(FluidVariant fluidVariant, List<Text> tooltip, TooltipContext tooltipContext) {
}
/**
* Return the sprite that should be used to render the passed fluid variant, for use in baked models, (block) entity renderers, or user interfaces.
*
* <p>Null may be returned if the fluid variant should not be rendered.
*/
@Nullable
default Sprite getSprite(FluidVariant fluidVariant) {
// Use the fluid render handler by default.
FluidRenderHandler fluidRenderHandler = FluidRenderHandlerRegistry.INSTANCE.get(fluidVariant.getFluid());
if (fluidRenderHandler != null) {
return fluidRenderHandler.getFluidSprites(null, null, fluidVariant.getFluid().getDefaultState())[0];
} else {
return null;
}
}
/**
* Return the color to use when rendering {@linkplain #getSprite the sprite} of this fluid variant.
*/
default int getColor(FluidVariant fluidVariant) {
// Use the fluid render handler by default.
FluidRenderHandler fluidRenderHandler = FluidRenderHandlerRegistry.INSTANCE.get(fluidVariant.getFluid());
if (fluidRenderHandler != null) {
return fluidRenderHandler.getFluidColor(null, null, fluidVariant.getFluid().getDefaultState());
} else {
return -1;
}
}
/**
* Return {@code true} if this fluid should fill tanks from top.
*/
default boolean fillsFromTop(FluidVariant fluidVariant) {
// By default, fluids should be filled from the bottom.
return false;
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.client.fluid;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.item.TooltipContext;
import net.minecraft.client.texture.Sprite;
import net.minecraft.fluid.Fluid;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.registry.Registry;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.lookup.v1.custom.ApiProviderMap;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
/**
* Client-side display of fluid variants.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
@Environment(EnvType.CLIENT)
public class FluidVariantRendering {
private static final ApiProviderMap<Fluid, FluidVariantRenderHandler> HANDLERS = ApiProviderMap.create();
private static final FluidVariantRenderHandler DEFAULT_HANDLER = new FluidVariantRenderHandler() { };
/**
* Register a render handler for the passed fluid.
*/
public static void register(Fluid fluid, FluidVariantRenderHandler handler) {
if (HANDLERS.putIfAbsent(fluid, handler) != null) {
throw new IllegalArgumentException("Duplicate handler registration for fluid " + fluid);
}
}
/**
* Return the render handler for the passed fluid, if available, and {@code null} otherwise.
*/
@Nullable
public static FluidVariantRenderHandler getHandler(Fluid fluid) {
return HANDLERS.get(fluid);
}
/**
* Return the render handler for the passed fluid, if available, or the default instance otherwise.
*/
public static FluidVariantRenderHandler getHandlerOrDefault(Fluid fluid) {
FluidVariantRenderHandler handler = HANDLERS.get(fluid);
return handler == null ? DEFAULT_HANDLER : handler;
}
/**
* Return the name of the passed fluid variant.
*/
public static Text getName(FluidVariant fluidVariant) {
return getHandlerOrDefault(fluidVariant.getFluid()).getName(fluidVariant);
}
/**
* Return the tooltip for the passed fluid variant, including the name and additional lines if available
* and the id of the fluid if advanced tooltips are enabled.
*/
public static List<Text> getTooltip(FluidVariant fluidVariant, TooltipContext context) {
List<Text> tooltip = new ArrayList<>();
// Name first
tooltip.add(getName(fluidVariant));
// Additional tooltip information
getHandlerOrDefault(fluidVariant.getFluid()).appendTooltip(fluidVariant, tooltip, context);
// If advanced tooltips are enabled, render the fluid id
if (context.isAdvanced()) {
tooltip.add(new LiteralText(Registry.FLUID.getId(fluidVariant.getFluid()).toString()).formatted(Formatting.DARK_GRAY));
}
// TODO: consider adding an event to append to tooltips?
return tooltip;
}
/**
* Return the sprite that should be used to render the passed fluid variant, or null if it's not available.
* The sprite should be rendered using the color returned by {@link #getColor}.
*/
@Nullable
public static Sprite getSprite(FluidVariant fluidVariant) {
return getHandlerOrDefault(fluidVariant.getFluid()).getSprite(fluidVariant);
}
/**
* Return the color that should be used to render {@linkplain #getSprite the sprite} of the passed fluid variant.
*/
public static int getColor(FluidVariant fluidVariant) {
return getHandlerOrDefault(fluidVariant.getFluid()).getColor(fluidVariant);
}
/**
* Return {@code true} if this fluid variant should be rendered as filling tanks from the top.
*/
public static boolean fillsFromTop(FluidVariant fluidVariant) {
return getHandlerOrDefault(fluidVariant.getFluid()).fillsFromTop(fluidVariant);
}
}

View file

@ -0,0 +1,181 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.fluid;
import java.util.Collection;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.LeveledCauldronBlock;
import net.minecraft.fluid.Fluid;
import net.minecraft.fluid.Fluids;
import net.minecraft.state.property.IntProperty;
import net.fabricmc.fabric.api.lookup.v1.custom.ApiProviderMap;
import net.fabricmc.fabric.impl.transfer.fluid.CauldronStorage;
/**
* Entrypoint to expose cauldrons to the Fluid Transfer API.
* Empty, water and lava cauldrons are registered by default, and additional cauldrons must be registered with {@link #registerCauldron}.
* Contents can be queried with {@link #getForBlock} and {@link #getForFluid}.
*
* <p>The {@code CauldronFluidContent} itself defines:
* <ul>
* <li>The block of the cauldron.</li>
* <li>The fluid that can be accepted by the cauldron. NBT is discarded when entering the cauldron.</li>
* <li>Which fluid amounts can be stored in the cauldron, and how they map to the level property of the cauldron.
* If {@code levelProperty} is {@code null}, then {@code maxLevel = 1}, and there is only one level.
* Otherwise, the levels are all the integer values between {@code 1} and {@code maxLevel} (included).
* </li>
* <li>{@code amountPerLevel} defines how much fluid (in droplets) there is in one level of the cauldron.</li>
* </ul>
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public final class CauldronFluidContent {
/**
* Block of the cauldron.
*/
public final Block block;
/**
* Fluid stored inside the cauldron.
*/
public final Fluid fluid;
/**
* Amount in droplets for each level of {@link #levelProperty}.
*/
public final long amountPerLevel;
/**
* Maximum level for {@link #levelProperty}. {@code 1} if {@code levelProperty} is null, otherwise a number {@code >= 1}.
* The minimum level is always 1.
*/
public final int maxLevel;
/**
* Property storing the level of the cauldron. If it's null, only one level is possible.
*/
@Nullable
public final IntProperty levelProperty;
private CauldronFluidContent(Block block, Fluid fluid, long amountPerLevel, int maxLevel, @Nullable IntProperty levelProperty) {
this.block = block;
this.fluid = fluid;
this.amountPerLevel = amountPerLevel;
this.maxLevel = maxLevel;
this.levelProperty = levelProperty;
}
// Copy-on-write, identity semantics, null-checked.
private static final ApiProviderMap<Block, CauldronFluidContent> BLOCK_TO_CAULDRON = ApiProviderMap.create();
private static final ApiProviderMap<Fluid, CauldronFluidContent> FLUID_TO_CAULDRON = ApiProviderMap.create();
/**
* Get the cauldron fluid content for a cauldron block, or {@code null} if none was registered (yet).
*/
@Nullable
public static CauldronFluidContent getForBlock(Block block) {
return BLOCK_TO_CAULDRON.get(block);
}
/**
* Get the cauldron fluid content for a fluid, or {@code null} if no cauldron was registered for that fluid (yet).
*/
@Nullable
public static CauldronFluidContent getForFluid(Fluid fluid) {
return FLUID_TO_CAULDRON.get(fluid);
}
/**
* Attempt to register a new cauldron if not already registered, allowing it to be filled and emptied through the Fluid Transfer API.
* In both cases, return the content of the cauldron, either the existing one, or the newly registered one.
*
* @param block The block of the cauldron.
* @param fluid The fluid stored in this cauldron.
* @param amountPerLevel How much fluid is contained in one level of the cauldron, in {@linkplain FluidConstants droplets}.
* @param levelProperty The property used by the cauldron to store its levels. {@code null} if the cauldron only has one level.
*/
public static synchronized CauldronFluidContent registerCauldron(Block block, Fluid fluid, long amountPerLevel, @Nullable IntProperty levelProperty) {
CauldronFluidContent existingBlockData = BLOCK_TO_CAULDRON.get(block);
if (existingBlockData != null) {
return existingBlockData;
}
if (FLUID_TO_CAULDRON.get(fluid) != null) {
throw new IllegalArgumentException("Fluid already has a mapping for a different block."); // TODO better message
}
CauldronFluidContent data;
if (levelProperty == null) {
data = new CauldronFluidContent(block, fluid, amountPerLevel, 1, null);
} else {
Collection<Integer> levels = levelProperty.getValues();
if (levels.size() == 0) {
throw new RuntimeException("Cauldron should have at least one possible level.");
}
int minLevel = Integer.MAX_VALUE;
int maxLevel = 0;
for (int level : levels) {
minLevel = Math.min(minLevel, level);
maxLevel = Math.max(maxLevel, level);
}
if (minLevel != 1 || maxLevel < 1) {
throw new IllegalStateException("Minimum level should be 1, and maximum level should be >= 1.");
}
data = new CauldronFluidContent(block, fluid, amountPerLevel, maxLevel, levelProperty);
}
BLOCK_TO_CAULDRON.putIfAbsent(block, data);
FLUID_TO_CAULDRON.putIfAbsent(fluid, data);
FluidStorage.SIDED.registerForBlocks((world, pos, state, be, context) -> CauldronStorage.get(world, pos), block);
return data;
}
/**
* Return the current level of the cauldron given its block state, or 0 if it's an empty cauldron.
*/
public int currentLevel(BlockState state) {
if (fluid == Fluids.EMPTY) {
return 0;
} else if (levelProperty == null) {
return 1;
} else {
return state.get(levelProperty);
}
}
static {
// Vanilla registrations
CauldronFluidContent.registerCauldron(Blocks.CAULDRON, Fluids.EMPTY, FluidConstants.BUCKET, null);
CauldronFluidContent.registerCauldron(Blocks.WATER_CAULDRON, Fluids.WATER, FluidConstants.BOTTLE, LeveledCauldronBlock.LEVEL);
CauldronFluidContent.registerCauldron(Blocks.LAVA_CAULDRON, Fluids.LAVA, FluidConstants.BUCKET, null);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.fluid;
import org.jetbrains.annotations.ApiStatus;
/**
* Constants for fluid transfer. In general, 1 bucket = 81000 droplets = 1 block.
*
* <p>If you don't know how much droplets you should pick for a specific resource that has a block form,
* the convention is to use 81000 droplets for what is worth one block of that resource.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public final class FluidConstants {
public static final long BUCKET = 81000;
public static final long BOTTLE = 27000;
public static final long BLOCK = 81000;
public static final long INGOT = 9000;
public static final long NUGGET = 1000;
public static final long DROPLET = 1;
/**
* Convert a fraction of buckets into droplets.
*
* <p>For example, passing {@code (1, 3)} will return the 1/3 of a bucket as droplets, so 27000.
*
* @return The amount of droplets that the passed fraction is equivalent to.
* @throws IllegalArgumentException If the fraction can't be converted to droplets exactly.
*/
public static long fromBucketFraction(long numerator, long denominator) {
long total = numerator * BUCKET;
if (total % denominator != 0) {
throw new IllegalArgumentException("Not a valid number of droplets!");
} else {
return total / denominator;
}
}
private FluidConstants() {
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.fluid;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.fluid.Fluids;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
/**
* Access to {@link Storage Storage&lt;FluidVariant&gt;} instances.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public final class FluidStorage {
/**
* Sided block access to fluid variant storages.
* Fluid amounts are always expressed in {@linkplain FluidConstants droplets}.
* The {@code Direction} parameter may never be null.
* Refer to {@link BlockApiLookup} for documentation on how to use this field.
*
* <p>When the operations supported by a storage change,
* that is if the return value of {@link Storage#supportsInsertion} or {@link Storage#supportsExtraction} changes,
* the storage should notify its neighbors with a block update so that they can refresh their connections if necessary.
*
* <p>May only be queried on the logical server thread, never client-side or from another thread!
*/
public static final BlockApiLookup<Storage<FluidVariant>, Direction> SIDED =
BlockApiLookup.get(new Identifier("fabric:sided_fluid_storage"), Storage.asClass(), Direction.class);
private FluidStorage() {
}
static {
// Initialize vanilla cauldron wrappers
CauldronFluidContent.getForFluid(Fluids.WATER);
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.fluid;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.fluid.Fluid;
import net.minecraft.fluid.Fluids;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.fabricmc.fabric.api.transfer.v1.client.fluid.FluidVariantRendering;
import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant;
import net.fabricmc.fabric.impl.transfer.fluid.FluidVariantImpl;
/**
* An immutable association of a still fluid and an optional NBT tag.
*
* <p>Do not extend this class. Use {@link #of(Fluid)} and {@link #of(Fluid, NbtCompound)} to create instances.
*
* <p>{@link FluidVariantRendering} can be used for client-side rendering of fluid variants.
*
* <p><b>Fluid variants must always be compared with {@link #equals}, never by reference!</b>
* {@link #hashCode} is guaranteed to be correct and constant time independently of the size of the NBT.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
@ApiStatus.NonExtendable
public interface FluidVariant extends TransferVariant<Fluid> {
/**
* Retrieve a blank FluidVariant.
*/
static FluidVariant blank() {
return of(Fluids.EMPTY);
}
/**
* Retrieve a FluidVariant with a fluid, and a {@code null} tag.
*/
static FluidVariant of(Fluid fluid) {
return of(fluid, null);
}
/**
* Retrieve a FluidVariant with a fluid, and an optional tag.
*/
static FluidVariant of(Fluid fluid, @Nullable NbtCompound nbt) {
return FluidVariantImpl.of(fluid, nbt);
}
/**
* Return the fluid of this variant.
*/
default Fluid getFluid() {
return getObject();
}
/**
* Deserialize a variant from an NBT compound tag, assuming it was serialized using {@link #toNbt}.
*
* <p>If an error occurs during deserialization, it will be logged with the DEBUG level, and a blank variant will be returned.
*/
static FluidVariant fromNbt(NbtCompound nbt) {
return FluidVariantImpl.fromNbt(nbt);
}
/**
* Read a variant from a packet byte buffer, assuming it was serialized using {@link #toPacket}.
*/
static FluidVariant fromPacket(PacketByteBuf buf) {
return FluidVariantImpl.fromPacket(buf);
}
}

View file

@ -0,0 +1,153 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.fluid.base;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.base.ResourceAmount;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
/**
* A storage that can store a single fluid variant at any given time.
* Implementors should at least override {@link #getCapacity(FluidVariant)}, and probably {@link #markDirty} as well.
*
* <p>{@link #canInsert} and {@link #canExtract} can be used for more precise control over which fluids may be inserted or extracted.
* If one of these two functions is overridden to always return false, implementors may also wish to override
* {@link #supportsInsertion} and/or {@link #supportsExtraction}.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public abstract class SingleFluidStorage extends SnapshotParticipant<ResourceAmount<FluidVariant>> implements SingleSlotStorage<FluidVariant> {
public FluidVariant fluidVariant = FluidVariant.blank();
public long amount;
/**
* Implement if you want.
*/
protected void markDirty() {
}
/**
* @return {@code true} if the passed non-blank fluid variant can be inserted, {@code false} otherwise.
*/
protected boolean canInsert(FluidVariant fluidVariant) {
return true;
}
/**
* @return {@code true} if the passed non-blank fluid variant can be extracted, {@code false} otherwise.
*/
protected boolean canExtract(FluidVariant fluidVariant) {
return true;
}
/**
* @return The maximum capacity of this storage for the passed fluid variant.
* If the passed fluid variant is blank, an estimate should be returned.
*/
protected abstract long getCapacity(FluidVariant fluidVariant);
@Override
public final boolean isResourceBlank() {
return fluidVariant.isBlank();
}
@Override
public final FluidVariant getResource() {
return fluidVariant;
}
@Override
public final long getAmount() {
return fluidVariant.isBlank() ? 0 : amount;
}
@Override
public final long getCapacity() {
return getCapacity(fluidVariant);
}
@Override
public final long insert(FluidVariant insertedFluid, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(insertedFluid, maxAmount);
if ((insertedFluid.equals(fluidVariant) || fluidVariant.isBlank()) && canInsert(insertedFluid)) {
long insertedAmount = Math.min(maxAmount, getCapacity(insertedFluid) - amount);
if (insertedAmount > 0) {
updateSnapshots(transaction);
// Just in case.
if (fluidVariant.isBlank()) {
amount = 0;
}
amount += insertedAmount;
fluidVariant = insertedFluid;
}
return insertedAmount;
}
return 0;
}
@Override
public final long extract(FluidVariant extractedFluid, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(extractedFluid, maxAmount);
if (extractedFluid.equals(fluidVariant) && canExtract(extractedFluid)) {
long extractedAmount = Math.min(maxAmount, amount);
if (extractedAmount > 0) {
updateSnapshots(transaction);
amount -= extractedAmount;
if (amount == 0) {
fluidVariant = FluidVariant.blank();
}
}
return extractedAmount;
}
return 0;
}
@Override
protected final ResourceAmount<FluidVariant> createSnapshot() {
return new ResourceAmount<>(fluidVariant, amount);
}
@Override
protected final void readSnapshot(ResourceAmount<FluidVariant> snapshot) {
this.fluidVariant = snapshot.resource();
this.amount = snapshot.amount();
}
@Override
protected final void onFinalCommit() {
markDirty();
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <h1>The Transfer API, version 1.</h1>
*
* <p>This module provides common facilities for the transfer of fluids and other game resources.
*
* <p><h2>Transactions</h2>
* The {@link net.fabricmc.fabric.api.transfer.v1.transaction.Transaction Transaction} system provides a
* scope that can be used to simulate any number of transfer operations, and then cancel or validate all of them at once.
* One can think of transactions as video game checkpoints. A more detailed explanation can be found in the class javadoc of {@code Transaction}.
* Every transfer operation requires a {@code Transaction} parameter.
* {@link net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant SnapshotParticipant}
* is the reference implementation of a "participant", that is an object participating in a transaction.
* </p>
*
* <p><h2>Storages</h2>
* A {@link net.fabricmc.fabric.api.transfer.v1.storage.Storage Storage&lt;T&gt;} is any object that can store resources of type {@code T}.
* Its contents can be read, and resources can be inserted into it or extracted from it.
* {@link net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil StorageUtil} provides a few helpful function to work with {@code Storage}s,
* for example to move resources between two {@code Storage}s.
* The {@link net.fabricmc.fabric.api.transfer.v1.storage.base storage/base package} provides a few helpers to accelerate
* implementation of {@code Storage&lt;T&gt;}.
* Usage of {@link net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions StoragePreconditions} is recommended to detect
* wrong usage of {@code Storage} and {@code StorageView} methods.
* </p>
*
* <p><h2>Fluid transfer</h2>
* A {@code Storage<FluidVariant>} is any object that can store fluids. It is just a {@code Storage<T>}, where {@code T} is
* {@link net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant FluidVariant}, the immutable combination of a {@code Fluid} and additional NBT data.
* Instances can be accessed through the API lookup defined in {@link net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage FluidStorage}.
* </p>
*
* <p>Implementors of fluid inventories with a fixed number of "slots" or "tanks" can use
* {@link net.fabricmc.fabric.api.transfer.v1.fluid.base.SingleFluidStorage SingleFluidStorage},
* and combine them with {@link net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage CombinedStorage}.
*
* <p>The amount for fluid transfer is droplets, that is 1/81000ths of a bucket.
* {@link net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants FluidConstants} contains a few helpful constants to work with droplets.
*
* <p>Client-side {@linkplain net.fabricmc.fabric.api.transfer.v1.client.fluid.FluidVariantRendering fluid variant rendering} will use regular fluid rendering by default,
* ignoring the additional NBT data.
* {@code Fluid}s that wish to render differently depending on the stored NBT data can register a
* {@link net.fabricmc.fabric.api.transfer.v1.client.fluid.FluidVariantRenderHandler FluidVariantRenderHandler}.
*/
package net.fabricmc.fabric.api.transfer.v1;

View file

@ -0,0 +1,217 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.fabric.api.transfer.v1.fluid.base.SingleFluidStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.ExtractionOnlyStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.InsertionOnlyStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleViewIterator;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import net.fabricmc.fabric.impl.transfer.TransferApiImpl;
/**
* An object that can store resources.
*
* <p><ul>
* <li>{@link #supportsInsertion} and {@link #supportsExtraction} can be used to tell if insertion and extraction
* functionality are possibly supported by this storage.</li>
* <li>{@link #insert} and {@link #extract} can be used to insert or extract resources from this storage.</li>
* <li>{@link #iterator} and {@link #exactView} can be used to inspect the contents of this storage.</li>
* <li>{@link #getVersion()} can be used to quickly check if a storage has changed, without having to rescan its contents.</li>
* </ul>
*
* <p>Users that wish to implement this interface can use the helpers in the {@code base} package:
* <ul>
* <li>{@link CombinedStorage} can be used to combine multiple instances, for example to combine multiple "slots" in one big storage.</li>
* <li>{@link ExtractionOnlyStorage} and {@link InsertionOnlyStorage} can be used when only extraction or insertion is needed.</li>
* <li>{@link SingleViewIterator} can be used to wrap a single view for use with {@link #iterator}.</li>
* <li>Resource-specific base implementations may also be available.
* For example, Fabric API provides {@link SingleFluidStorage} to accelerate implementations of {@code Storage<FluidVariant>}.</li>
* </ul>
*
* <p><b>Important note:</b> Unless otherwise specified, all transfer functions take a non-blank resource
* and a non-negative maximum amount as parameters.
* Implementations are encouraged to throw an exception if these preconditions are violated.
* {@link StoragePreconditions} can be used for these checks.
*
* <p>For transfer functions, the returned amount must be non-negative, and smaller than the passed maximum amount.
* Consumers of these functions are encourage to throw an exception if these postconditions are violated.
*
* @param <T> The type of the stored resources.
* @see Transaction
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public interface Storage<T> {
/**
* Return an empty storage.
*/
@SuppressWarnings("unchecked")
static <T> Storage<T> empty() {
return (Storage<T>) TransferApiImpl.EMPTY_STORAGE;
}
/**
* Return false if calling {@link #insert} will absolutely always return 0, or true otherwise or in doubt.
*
* <p>Note: This function is meant to be used by pipes or other devices that can transfer resources to know if
* they should interact with this storage at all.
*/
default boolean supportsInsertion() {
return true;
}
/**
* Try to insert up to some amount of a resource into this storage.
*
* @param resource The resource to insert. May not be blank.
* @param maxAmount The maximum amount of resource to insert. May not be negative.
* @param transaction The transaction this operation is part of.
* @return A nonnegative integer not greater than maxAmount: the amount that was inserted.
*/
long insert(T resource, long maxAmount, TransactionContext transaction);
/**
* Return false if calling {@link #extract} will absolutely always return 0, or true otherwise or in doubt.
*
* <p>Note: This function is meant to be used by pipes or other devices that can transfer resources to know if
* they should interact with this storage at all.
*/
default boolean supportsExtraction() {
return true;
}
/**
* Try to extract up to some amount of a resource from this storage.
*
* @param resource The resource to extract. May not be blank.
* @param maxAmount The maximum amount of resource to extract. May not be negative.
* @param transaction The transaction this operation is part of.
* @return A nonnegative integer not greater than maxAmount: the amount that was extracted.
*/
long extract(T resource, long maxAmount, TransactionContext transaction);
/**
* Iterate through the contents of this storage, for the scope of the passed transaction.
* Every visited {@link StorageView} represents a stored resource and an amount.
*
* <p>The returned iterator and any view it returns are only valid for the scope of to the passed transaction.
* They should not be used once that transaction is closed.
*
* <p>More precisely, as soon as the transaction is closed,
* {@link Iterator#hasNext hasNext()} must return {@code false},
* and any call to {@link Iterator#next next()} must throw a {@link NoSuchElementException}.
*
* <p>{@link #insert} and {@link #extract} may be called safely during iteration.
* Extractions should be visible to an open iterator, but insertions are not required to.
* In particular, inventories with a fixed amount of slots may wish to make insertions visible to iterators,
* but inventories with a dynamic or very large amount of slots should not do that to ensure timely termination of
* the iteration.
*
* @param transaction The transaction to which the scope of the returned iterator is tied.
* @return An iterator over the contents of this storage.
*/
Iterator<StorageView<T>> iterator(TransactionContext transaction);
/**
* Iterate through the contents of this storage, for the scope of the passed transaction.
* This function follows the semantics of {@link #iterator}, but returns an {@code Iterable} for use in {@code for} loops.
*
* @param transaction The transaction to which the scope of the returned iterator is tied.
* @return An iterable over the contents of this storage.
* @see #iterator
*/
default Iterable<StorageView<T>> iterable(TransactionContext transaction) {
return () -> iterator(transaction);
}
/**
* Return a view over this storage, for a specific resource, or {@code null} if none is quickly available.
*
* <p>This function should only return a non-null view if this storage can provide it quickly,
* for example with a hashmap lookup.
* If returning the requested view would require iteration through a potentially large number of views,
* {@code null} should be returned instead.
*
* <p>The returned view is tied to the passed transaction,
* and may never be used once the passed transaction has been closed.
*
* @param transaction The transaction to which the scope of the returned storage view is tied.
* @param resource The resource for which a storage view is requested. May be blank, for example to estimate capacity.
* @return A view over this storage for the passed resource, or {@code null} if none is quickly available.
*/
@Nullable
default StorageView<T> exactView(TransactionContext transaction, T resource) {
return null;
}
/**
* Return an integer representing the current version of this storage instance to allow for fast change detection:
* if the version hasn't changed since the last time, <b>and the storage instance is the same</b>, the storage has the same contents.
* This can be used to avoid re-scanning the contents of the storage, which could be an expensive operation.
* It may be used like that:
* <pre>{@code
* // Store storage and version:
* Storage<?> firstStorage = // ...
* long firstVersion = firstStorage.getVersion();
*
* // Later, check if the secondStorage is the unchanged firstStorage:
* Storage<?> secondStorage = // ...
* long secondVersion = secondStorage.getVersion();
* // We must check firstStorage == secondStorage first, otherwise versions may not be compared.
* if (firstStorage == secondStorage && firstVersion == secondVersion) {
* // storage contents are the same.
* } else {
* // storage contents might have changed.
* }
* }</pre>
*
* <p>The version <b>must</b> change if the state of the storage has changed,
* generally after a direct modification, or at the end of a modifying transaction.
* The version may also change even if the state of the storage hasn't changed.
*
* <p>It is not valid to call this during a transaction,
* and implementations are encouraged to throw an exception if that happens.
*/
default long getVersion() {
if (Transaction.isOpen()) {
throw new IllegalStateException("getVersion() may not be called during a transaction.");
}
return TransferApiImpl.version.getAndIncrement();
}
/**
* Return a class instance of this interface with the desired generic type,
* to be used for easier registration with API lookups.
*/
@SuppressWarnings("unchecked")
static <T> Class<Storage<T>> asClass() {
return (Class<Storage<T>>) (Object) Storage.class;
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage;
import org.jetbrains.annotations.ApiStatus;
/**
* Preconditions that can be used when working with storages.
*
* <p>In particular, {@link #notNegative} or {@link #notBlankNotNegative} can be used by implementations of
* {@link Storage#insert} and {@link Storage#extract} to fail-fast if the arguments are invalid.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public class StoragePreconditions {
/**
* Ensure that the passed transfer variant is not blank.
*
* @throws IllegalArgumentException If the variant is blank.
*/
public static void notBlank(TransferVariant<?> variant) {
if (variant.isBlank()) {
throw new IllegalArgumentException("Transfer variant may not be blank.");
}
}
/**
* Ensure that the passed amount is not negative. That is, it must be {@code >= 0}.
*
* @throws IllegalArgumentException If the amount is negative.
*/
public static void notNegative(long amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount may not be negative, but it is: " + amount);
}
}
/**
* Check both for a not blank transfer variant and a not negative amount.
*/
public static void notBlankNotNegative(TransferVariant<?> variant, long amount) {
notBlank(variant);
notNegative(amount);
}
private StoragePreconditions() {
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage;
import java.util.function.Predicate;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* Helper functions to work with {@link Storage}s.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public final class StorageUtil {
/**
* Move resources between two storages, matching the passed filter, and return the amount that was successfully transferred.
*
* <p>Here is a usage example with fluid variant storages:
* <pre>{@code
* // Source
* Storage<FluidVariant> source;
* // Target
* Storage<FluidVariant> target;
*
* // Move up to one bucket in total from source to target, outside of a transaction:
* long amountMoved = StorageUtil.move(source, target, variant -> true, FluidConstants.BUCKET, null);
* // Move exactly one bucket in total, only of water:
* try (Transaction transaction = Transaction.openOuter()) {
* Predicate<FluidVariant> filter = variant -> variant.isOf(Fluids.WATER);
* long waterMoved = StorageUtil.move(source, target, filter, FluidConstants.BUCKET, transaction);
* if (waterMoved == FluidConstants.BUCKET) {
* // Only commit if exactly one bucket was moved (no less!).
* transaction.commit();
* }
* }
* }</pre>
*
* @param from The source storage. May be null.
* @param to The target storage. May be null.
* @param filter The filter for transferred resources.
* Only resources for which this filter returns {@code true} will be transferred.
* This filter will never be tested with a blank resource, and filters are encouraged to throw an
* exception if this guarantee is violated.
* @param maxAmount The maximum amount that will be transferred.
* @param transaction The transaction this transfer is part of, or {@code null} if a transaction should be opened just for this transfer.
* @param <T> The type of resources to move.
* @return The total amount of resources that was successfully transferred.
* @throws IllegalStateException If no transaction is passed and a transaction is already active on the current thread.
*/
public static <T> long move(@Nullable Storage<T> from, @Nullable Storage<T> to, Predicate<T> filter, long maxAmount, @Nullable TransactionContext transaction) {
if (from == null || to == null) return 0;
long totalMoved = 0;
try (Transaction iterationTransaction = (transaction == null ? Transaction.openOuter() : transaction.openNested())) {
for (StorageView<T> view : from.iterable(iterationTransaction)) {
if (view.isResourceBlank()) continue;
T resource = view.getResource();
if (!filter.test(resource)) continue;
long maxExtracted;
// check how much can be extracted
try (Transaction extractionTestTransaction = iterationTransaction.openNested()) {
maxExtracted = view.extract(resource, maxAmount - totalMoved, extractionTestTransaction);
extractionTestTransaction.abort();
}
try (Transaction transferTransaction = iterationTransaction.openNested()) {
// check how much can be inserted
long accepted = to.insert(resource, maxExtracted, transferTransaction);
// extract it, or rollback if the amounts don't match
if (view.extract(resource, accepted, transferTransaction) == accepted) {
totalMoved += accepted;
transferTransaction.commit();
}
}
if (maxAmount == totalMoved) {
// early return if nothing can be moved anymore
iterationTransaction.commit();
return totalMoved;
}
}
iterationTransaction.commit();
}
return totalMoved;
}
/**
* Attempt to find a resource stored in the passed storage.
*
* @param storage The storage to inspect, may be null.
* @param transaction The current transaction, or {@code null} if a transaction should be opened for this query.
* @param <T> The type of the stored resources.
* @return A non-blank resource stored in the storage, or {@code null} if none could be found.
*/
@Nullable
public static <T> T findStoredResource(@Nullable Storage<T> storage, @Nullable TransactionContext transaction) {
if (storage == null) return null;
if (transaction == null) {
try (Transaction outer = Transaction.openOuter()) {
return findStoredResourceInner(storage, outer);
}
} else {
return findStoredResourceInner(storage, transaction);
}
}
@Nullable
private static <T> T findStoredResourceInner(Storage<T> storage, TransactionContext transaction) {
for (StorageView<T> view : storage.iterable(transaction)) {
if (!view.isResourceBlank()) {
return view.getResource();
}
}
return null;
}
/**
* Attempt to find a resource stored in the passed storage that can be extracted.
*
* @param storage The storage to inspect, may be null.
* @param transaction The current transaction, or {@code null} if a transaction should be opened for this query.
* @param <T> The type of the stored resources.
* @return A non-blank resource stored in the storage that can be extracted, or {@code null} if none could be found.
*/
@Nullable
public static <T> T findExtractableResource(@Nullable Storage<T> storage, @Nullable TransactionContext transaction) {
if (storage == null) return null;
try (Transaction nested = transaction == null ? Transaction.openOuter() : transaction.openNested()) {
for (StorageView<T> view : storage.iterable(nested)) {
// Extract below could change the resource, so we have to query it before extracting.
T resource = view.getResource();
if (!view.isResourceBlank() && view.extract(resource, Long.MAX_VALUE, nested) > 0) {
// Will abort the extraction.
return resource;
}
}
}
return null;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A view of a single stored resource in a {@link Storage}, for use with {@link Storage#iterator} or {@link Storage#exactView}.
*
* <p>A view is always tied to a specific transaction, and should not be accessed outside of it.
*
* @param <T> The type of the stored resource.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public interface StorageView<T> {
/**
* Try to extract a resource from this view.
*
* @return The amount that was extracted.
*/
long extract(T resource, long maxAmount, TransactionContext transaction);
/**
* @return {@code true} if the {@link #getResource} contained in this storage view is blank, or {@code false} otherwise.
*/
boolean isResourceBlank();
/**
* @return The resource stored in this view. May not be blank if {@link #isResourceBlank} is {@code false}.
*/
T getResource();
/**
* @return The amount of {@link #getResource} stored in this view.
*/
long getAmount();
/**
* @return The total amount of {@link #getResource} that could be stored in this view,
* or an estimate of the number of resources that could be stored if this view has a blank resource.
*/
long getCapacity();
}

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage;
import java.util.Objects;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
/**
* An immutable association of an immutable object instance (for example {@code Item} or {@code Fluid}) and an optional NBT tag.
*
* <p>This is exposed for convenience for code that needs to be generic across multiple transfer variants,
* but note that a {@link Storage} is not necessarily bound to {@code TransferVariant}. Its generic parameter can be any immutable object.
*
* <p><b>Transfer variants must always be compared with {@link #equals}, never by reference!</b>
* {@link #hashCode} is guaranteed to be correct and constant time independently of the size of the NBT.
*
* @param <O> The type of the immutable object instance, for example {@code Item} or {@code Fluid}.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public interface TransferVariant<O> {
/**
* Return true if this variant is blank, and false otherwise.
*/
boolean isBlank();
/**
* Return the immutable object instance of this variant.
*/
O getObject();
/**
* Return the underlying tag.
*
* <p><b>NEVER MUTATE THIS NBT TAG</b>, if you need to mutate it you can use {@link #copyNbt()} to retrieve a copy instead.
*/
@Nullable
NbtCompound getNbt();
/**
* Return true if this variant has a tag, false otherwise.
*/
default boolean hasNbt() {
return getNbt() != null;
}
/**
* Return true if the tag of this variant matches the passed tag, and false otherwise.
*
* <p>Note: True is returned if both tags are {@code null}.
*/
default boolean nbtMatches(@Nullable NbtCompound other) {
return Objects.equals(getNbt(), other);
}
/**
* Return {@code true} if the object of this variant matches the passed fluid.
*/
default boolean isOf(O object) {
return getObject() == object;
}
/**
* Return a copy of the tag of this variant, or {@code null} if this variant doesn't have a tag.
*
* <p>Note: Use {@link #nbtMatches} if you only need to check for custom tag equality, or {@link #getNbt()} if you don't need to mutate the tag.
*/
@Nullable
default NbtCompound copyNbt() {
NbtCompound nbt = getNbt();
return nbt == null ? null : nbt.copy();
}
/**
* Save this variant into an NBT compound tag. Subinterfaces should have a matching static {@code fromNbt}.
*
* <p>Note: This is safe to use for persisting data as objects are saved using their full Identifier.
*/
NbtCompound toNbt();
/**
* Write this variant into a packet byte buffer. Subinterfaces should have a matching static {@code fromPacket}.
*
* <p>Implementation note: Objects are saved using their raw registry integer id.
*/
void toPacket(PacketByteBuf buf);
}

View file

@ -0,0 +1,161 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage.base;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
/**
* A {@link Storage} wrapping multiple storages.
*
* <p>The storages passed to {@linkplain CombinedStorage#CombinedStorage the constructor} will be iterated in order.
*
* @param <T> The type of the stored resources.
* @param <S> The class of every part. {@code ? extends Storage<T>} can be used if the parts are of different types.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public class CombinedStorage<T, S extends Storage<T>> implements Storage<T> {
public final List<S> parts;
public CombinedStorage(List<S> parts) {
this.parts = parts;
}
@Override
public boolean supportsInsertion() {
for (S part : parts) {
if (part.supportsInsertion()) {
return true;
}
}
return false;
}
@Override
public long insert(T resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notNegative(maxAmount);
long amount = 0;
for (S part : parts) {
amount += part.insert(resource, maxAmount - amount, transaction);
if (amount == maxAmount) break;
}
return amount;
}
@Override
public boolean supportsExtraction() {
for (S part : parts) {
if (part.supportsExtraction()) {
return true;
}
}
return false;
}
@Override
public long extract(T resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notNegative(maxAmount);
long amount = 0;
for (S part : parts) {
amount += part.extract(resource, maxAmount - amount, transaction);
if (amount == maxAmount) break;
}
return amount;
}
@Override
public Iterator<StorageView<T>> iterator(TransactionContext transaction) {
return new CombinedIterator(transaction);
}
/**
* The combined iterator for multiple storages.
*/
private class CombinedIterator implements Iterator<StorageView<T>>, Transaction.CloseCallback {
boolean open = true;
final TransactionContext transaction;
final Iterator<S> partIterator = parts.iterator();
// Always holds the next StorageView<T>, except during next() while the iterator is being advanced.
Iterator<StorageView<T>> currentPartIterator = null;
CombinedIterator(TransactionContext transaction) {
this.transaction = transaction;
advanceCurrentPartIterator();
transaction.addCloseCallback(this);
}
@Override
public boolean hasNext() {
return open && currentPartIterator != null && currentPartIterator.hasNext();
}
@Override
public StorageView<T> next() {
if (!open) {
throw new NoSuchElementException("The transaction for this iterator was closed.");
}
if (!hasNext()) {
throw new NoSuchElementException();
}
StorageView<T> returned = currentPartIterator.next();
// Advance the current part iterator
if (!currentPartIterator.hasNext()) {
advanceCurrentPartIterator();
}
return returned;
}
private void advanceCurrentPartIterator() {
while (partIterator.hasNext()) {
this.currentPartIterator = partIterator.next().iterator(transaction);
if (this.currentPartIterator.hasNext()) {
break;
}
}
}
@Override
public void onClose(TransactionContext transaction, Transaction.Result result) {
// As soon as the transaction is closed, this iterator is not valid anymore.
open = false;
}
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage.base;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A {@link Storage} that supports extraction, and not insertion.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public interface ExtractionOnlyStorage<T> extends Storage<T> {
@Override
default boolean supportsInsertion() {
return false;
}
@Override
default long insert(T resource, long maxAmount, TransactionContext transaction) {
return 0;
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage.base;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A {@link Storage} that supports insertion, and not extraction.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public interface InsertionOnlyStorage<T> extends Storage<T> {
@Override
default boolean supportsExtraction() {
return false;
}
@Override
default long extract(T resource, long maxAmount, TransactionContext transaction) {
return 0;
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage.base;
import org.jetbrains.annotations.ApiStatus;
/**
* An immutable object storing both a resource and an amount, provided for convenience.
* @param <T> The type of the stored resource.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public record ResourceAmount<T> (T resource, long amount) {
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage.base;
import java.util.Iterator;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A storage that is also its only storage view.
* It can be used in APIs for storages that are wrappers around a single "slot", or for slightly more convenient implementation.
*
* @param <T> The type of the stored resource.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public interface SingleSlotStorage<T> extends Storage<T>, StorageView<T> {
@Override
default Iterator<StorageView<T>> iterator(TransactionContext transaction) {
return SingleViewIterator.create(this, transaction);
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.storage.base;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* An iterator for a single {@link StorageView}, tied to a transaction. Instances can be created with {@link #create}.
*
* <p>This class should only be used by implementors of {@link Storage#iterator}, that wish to expose a single storage view.
* In that case, usage of this class is recommended, as it will ensure that the storage view can't be accessed after the transaction is closed.
*
* @param <T> The type of the stored resource.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public final class SingleViewIterator<T> implements Iterator<StorageView<T>>, Transaction.CloseCallback {
/**
* Create a new iterator for the passed storage view, tied to the passed transaction.
*
* <p>The iterator will ensure that it can only be used as long as the transaction is open.
*/
public static <T> Iterator<StorageView<T>> create(StorageView<T> view, TransactionContext transaction) {
SingleViewIterator<T> it = new SingleViewIterator<>(view);
transaction.addCloseCallback(it);
return it;
}
private boolean open = true;
private boolean hasNext = true;
private final StorageView<T> view;
private SingleViewIterator(StorageView<T> view) {
this.view = view;
}
@Override
public boolean hasNext() {
return open && hasNext;
}
@Override
public StorageView<T> next() {
if (!open) {
throw new NoSuchElementException("The transaction for this iterator was closed.");
}
if (!hasNext()) {
throw new NoSuchElementException();
}
hasNext = false;
return view;
}
@Override
public void onClose(TransactionContext transaction, Transaction.Result result) {
open = false;
}
}

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.transaction;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
import net.fabricmc.fabric.impl.transfer.transaction.TransactionManagerImpl;
/**
* A global operation where participants guarantee atomicity: either the whole operation succeeds,
* or it is completely aborted and rolled back.
*
* <p>One can imagine that transactions are like video game checkpoints.
* <ul>
* <li>{@linkplain #openOuter Opening a transaction} with a try-with-resources block creates a checkpoint.</li>
* <li>Modifications to game state can then happen.</li>
* <li>Calling {@link #commit} validates the modifications that happened during the transaction,
* essentially discarding the checkpoint.</li>
* <li>Calling {@link #abort} or doing nothing and letting the transaction be {@linkplain #close closed} at the end
* of the try-with-resources block cancels any modification that happened during the transaction,
* reverting to the checkpoint.</li>
* <li>Calling {@link #openNested} on a transaction creates a new nested transaction, i.e. a new checkpoint with the current state.
* Committing a nested transaction will validate the changes that happened, but they may
* still be cancelled later if a parent transaction is cancelled.
* Aborting a nested transaction immediately reverts the changes - cancelling any modification made after the call
* to {@link #openNested}.</li>
* </ul>
*
* <p>This is illustrated in the following example.
* <pre>{@code
* try (Transaction outerTransaction = Transaction.openOuter()) {
* // (A) some transaction operations
* try (Transaction nestedTransaction = outerTransaction.openNested()) {
* // (B) more operations
* nestedTransaction.commit(); // Validate the changes that happened in this transaction.
* // This is a nested transaction, so changes will only be applied if the outer
* // transaction is committed too.
* }
* // (C) even more operations
* outerTransaction.commit(); // This is an outer transaction: changes (A), (B) and (C) are applied.
* }
* // If we hadn't committed the outerTransaction, all changes (A), (B) and (C) would have been reverted.
* }</pre>
*
* <p>Participants are responsible for upholding this contract themselves, by using {@link #addCloseCallback}
* to react to transaction close events and properly validate or revert changes.
* Any action that modifies state outside of the transaction, such as calls to {@code markDirty()} or neighbor updates,
* should be deferred until {@linkplain #addOuterCloseCallback after the outer transaction is closed}
* to give every participant a chance to react to transaction close events.
*
* <p>This is very low-level for most applications, and most participants should subclass {@link SnapshotParticipant}
* that will take care of properly maintaining their state.
*
* <p>Participants should generally be passed a {@link TransactionContext} parameter instead of the full {@code Transaction},
* to make sure they don't call {@link #abort}, {@link #commit} or {@link #close} mistakenly.
*
* <p>Every transaction is only valid on the thread it was opened on,
* and attempts to use it on another thread will throw an exception.
* Consequently, transactions can be concurrent across multiple threads, as long as they don't share any state.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
@ApiStatus.NonExtendable
public interface Transaction extends AutoCloseable, TransactionContext {
/**
* Open a new outer transaction.
*
* @throws IllegalStateException If a transaction is already active on the current thread.
*/
static Transaction openOuter() {
return TransactionManagerImpl.MANAGERS.get().openOuter();
}
/**
* @return True if a transaction is open on the current thread, and false otherwise.
*/
static boolean isOpen() {
return TransactionManagerImpl.MANAGERS.get().isOpen();
}
/**
* Close the current transaction, rolling back all the changes that happened during this transaction and
* the transactions opened with {@link #openNested} from this transaction.
*
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
* @throws IllegalStateException If this transaction is not the current transaction.
* @throws IllegalStateException If this transaction was closed.
*/
void abort();
/**
* Close the current transaction, committing all the changes that happened during this transaction and
* the <b>committed</b> transactions opened with {@link #openNested} from this transaction.
* If this transaction was opened with {@link #openOuter}, all changes are applied.
* If this transaction was opened with {@link #openNested}, all changes will be applied when and if the changes of
* the parent transactions are applied.
*
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
* @throws IllegalStateException If this transaction is not the current transaction.
* @throws IllegalStateException If this transaction was closed.
*/
void commit();
/**
* Abort the current transaction if it was not closed already.
*/
@Override
void close();
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.transaction;
import org.jetbrains.annotations.ApiStatus;
/**
* A subset of a {@link Transaction} that lets participants properly take part in transactions, manage their state,
* or open nested transactions, but does not allow them to close the transaction they are passed.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
@ApiStatus.NonExtendable
public interface TransactionContext {
/**
* Open a new nested transaction.
*
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
* @throws IllegalStateException If this transaction is not the current transaction.
* @throws IllegalStateException If this transaction was closed.
*/
Transaction openNested();
/**
* @return The nesting depth of this transaction: 0 if it was opened with {@link Transaction#openOuter},
* 1 if its parent was opened with {@link Transaction#openOuter}, and so on...
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
*/
int nestingDepth();
/**
* Return the transaction with the specific nesting depth.
*
* @param nestingDepth Queried nesting depth.
* @throws IndexOutOfBoundsException If there is no open transaction with the request nesting depth.
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
*/
Transaction getOpenTransaction(int nestingDepth);
/**
* Register a callback that will be invoked when this transaction is closed.
* Registered callbacks are invoked last-to-first: the last callback to be registered will be the first to be invoked, and so on...
*
* <p>Updates that may change the state of other participants should be deferred until after the outermost transaction is closed
* using {@link #addOuterCloseCallback}.
*
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
*/
void addCloseCallback(CloseCallback closeCallback);
/**
* A callback that is invoked when a transaction is closed.
*/
@FunctionalInterface
interface CloseCallback {
/**
* Perform an action when a transaction is closed.
*
* @param transaction The closed transaction. Only {@link #nestingDepth}, {@link #getOpenTransaction} and {@link #addOuterCloseCallback}
* may be called on that transaction.
* {@link #addCloseCallback} may additionally be called on parent transactions
* (accessed through {@link #getOpenTransaction} for lower nesting depths).
* @param result The result of this transaction: whether it was committed or aborted.
*/
void onClose(TransactionContext transaction, Result result);
}
/**
* Register a callback that will be invoked after the outermost transaction is closed,
* and after callbacks registered with {@link #addCloseCallback} are ran.
* Registered callbacks are invoked last-to-first.
*
* @throws IllegalStateException If this function is not called on the thread this transaction was opened in.
*/
void addOuterCloseCallback(OuterCloseCallback outerCloseCallback);
/**
* A callback that is invoked after the outer transaction is closed.
*/
@FunctionalInterface
interface OuterCloseCallback {
/**
* Perform an action after the top-level transaction is closed.
*
* @param result The result of the top-level transaction.
*/
void afterOuterClose(Result result);
}
/**
* The result of a transaction operation.
*/
enum Result {
ABORTED,
COMMITTED;
/**
* @return true if the transaction was aborted, false if it was committed.
*/
public boolean wasAborted() {
return this == ABORTED;
}
/**
* @return true if the transaction was committed, false if it was aborted.
*/
public boolean wasCommitted() {
return this == COMMITTED;
}
}
}

View file

@ -0,0 +1,129 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.transfer.v1.transaction.base;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A base participant implementation that modifies itself during transactions,
* saving snapshots of its state in objects of type {@code T} in case it needs to revert to a previous state.
*
* <p>{@link #updateSnapshots} should be called before any modification.
* This will save the state of this participant using {@link #createSnapshot} if no state was already saved for that transaction.
* When the transaction is aborted and changes need to be rolled back, {@link #readSnapshot} will be called
* to signal that the current state should revert to that of the snapshot.
* The snapshot object is then {@linkplain #releaseSnapshot released}, and can be cached for subsequent use, or discarded.
*
* <p>When an outer transaction is committed, {@link #readSnapshot} will not be called so that the current state of this participant
* is retained. {@link #releaseSnapshot} will be called because the snapshot is not necessary anymore,
* and {@link #onFinalCommit} will be called after the transaction is closed.
*
* @param <T> The objects that this participant uses to save its state snapshots.
*
* @deprecated Experimental feature, we reserve the right to remove or change it without further notice.
* The transfer API is a complex addition, and we want to be able to correct possible design mistakes.
*/
@ApiStatus.Experimental
@Deprecated
public abstract class SnapshotParticipant<T> implements Transaction.CloseCallback, Transaction.OuterCloseCallback {
private final List<T> snapshots = new ArrayList<>();
/**
* Return a new <b>nonnull</b> object containing the current state of this participant.
* <b>{@code null} may not be returned, or an exception will be thrown!</b>
*/
protected abstract T createSnapshot();
/**
* Roll back to a state previously created by {@link #createSnapshot}.
*/
protected abstract void readSnapshot(T snapshot);
/**
* Signals that the snapshot will not be used anymore, and is safe to cache for next calls to {@link #createSnapshot},
* or discard entirely.
*/
protected void releaseSnapshot(T snapshot) {
}
/**
* Called after an outer transaction succeeded,
* to perform irreversible actions such as {@code markDirty()} or neighbor updates.
*/
protected void onFinalCommit() {
}
/**
* Update the stored snapshots so that the changes happening as part of the passed transaction can be correctly
* committed or rolled back.
* This function should be called every time the participant is about to change its internal state as part of a transaction.
*/
public final void updateSnapshots(TransactionContext transaction) {
// Make sure we have enough storage for snapshots
while (snapshots.size() <= transaction.nestingDepth()) {
snapshots.add(null);
}
// If the snapshot is null, we need to create it, and we need to register a callback.
if (snapshots.get(transaction.nestingDepth()) == null) {
T snapshot = createSnapshot();
Objects.requireNonNull(snapshot, "Snapshot may not be null!");
snapshots.set(transaction.nestingDepth(), snapshot);
transaction.addCloseCallback(this);
}
}
@Override
public final void onClose(TransactionContext transaction, Transaction.Result result) {
// Get and remove the relevant snapshot.
T snapshot = snapshots.set(transaction.nestingDepth(), null);
if (result.wasAborted()) {
// If the transaction was aborted, we just revert to the state of the snapshot.
readSnapshot(snapshot);
releaseSnapshot(snapshot);
} else if (transaction.nestingDepth() > 0) {
if (snapshots.get(transaction.nestingDepth() - 1) == null) {
// No snapshot yet, so move the snapshot one nesting level up.
snapshots.set(transaction.nestingDepth() - 1, snapshot);
// This is the first snapshot at this level: we need to call addCloseCallback.
transaction.getOpenTransaction(transaction.nestingDepth() - 1).addCloseCallback(this);
} else {
// There is already an older snapshot at the nesting level above, just release the newer one.
releaseSnapshot(snapshot);
}
} else {
releaseSnapshot(snapshot);
transaction.addOuterCloseCallback(this);
}
}
@Override
public final void afterOuterClose(Transaction.Result result) {
// The result is guaranteed to be COMMITTED,
// as this is only scheduled during onClose() when the outer transaction is successful.
onFinalCommit();
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.transfer;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicLong;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
public class TransferApiImpl {
public static final AtomicLong version = new AtomicLong();
@SuppressWarnings("rawtypes")
public static final Storage EMPTY_STORAGE = new Storage() {
@Override
public boolean supportsInsertion() {
return false;
}
@Override
public long insert(Object resource, long maxAmount, TransactionContext transaction) {
return 0;
}
@Override
public boolean supportsExtraction() {
return false;
}
@Override
public long extract(Object resource, long maxAmount, TransactionContext transaction) {
return 0;
}
@Override
public Iterator<StorageView> iterator(TransactionContext transaction) {
return Collections.emptyIterator();
}
@Override
public long getVersion() {
return 0;
}
};
}

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.transfer.fluid;
import java.util.Map;
import com.google.common.collect.MapMaker;
import com.google.common.primitives.Ints;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.fabricmc.fabric.api.transfer.v1.fluid.CauldronFluidContent;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
/**
* Standard implementation of {@code Storage<FluidVariant>}, using cauldron/fluid mappings registered in {@link CauldronFluidContent}.
*
* <p>Implementation notes:
* <ul>
* <li>To make sure multiple access to the same cauldron return the same wrapper, we maintain a {@code (World, BlockPos) -> Wrapper} cache.</li>
* <li>The wrapper mutates the world directly with setBlockState, but updates are suppressed.
* On final commit, a block update is sent by reverting to {@linkplain #lastReleasedSnapshot the initial block state} with updates suppressed,
* then setting the final block state again, without suppressing updates.</li>
* </ul>
*/
public class CauldronStorage extends SnapshotParticipant<BlockState> implements SingleSlotStorage<FluidVariant> {
// Record is used for convenient constructor, hashcode and equals implementations.
private record WorldLocation(World world, BlockPos pos) {
}
// Weak values to make sure wrappers are cleaned up after use, thread-safe.
private static final Map<WorldLocation, CauldronStorage> CAULDRONS = new MapMaker().concurrencyLevel(1).weakValues().makeMap();
public static CauldronStorage get(World world, BlockPos pos) {
WorldLocation location = new WorldLocation(world, pos.toImmutable());
CAULDRONS.computeIfAbsent(location, CauldronStorage::new);
return CAULDRONS.get(location);
}
private final WorldLocation location;
// this is the last released snapshot, which means it's the first snapshot ever saved when onFinalCommit() is called.
private BlockState lastReleasedSnapshot;
CauldronStorage(WorldLocation location) {
this.location = location;
}
@Override
protected void releaseSnapshot(BlockState snapshot) {
lastReleasedSnapshot = snapshot;
}
// Retrieve the current CauldronFluidContent.
private CauldronFluidContent getCurrentContent() {
CauldronFluidContent content = CauldronFluidContent.getForBlock(createSnapshot().getBlock());
if (content == null) {
throw new IllegalStateException("Unexpected error: no cauldron at location " + location);
}
return content;
}
// Called by insert and extract to update the block state.
private void updateLevel(CauldronFluidContent newContent, int level, TransactionContext transaction) {
updateSnapshots(transaction);
BlockState newState = newContent.block.getDefaultState();
if (newContent.levelProperty != null) {
newState = newState.with(newContent.levelProperty, level);
}
// Set block state without updates.
location.world.setBlockState(location.pos, newState, 0);
}
@Override
public long insert(FluidVariant fluidVariant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(fluidVariant, maxAmount);
CauldronFluidContent insertContent = CauldronFluidContent.getForFluid(fluidVariant.getFluid());
if (insertContent != null) {
int maxLevelsInserted = Ints.saturatedCast(maxAmount / insertContent.amountPerLevel);
if (getAmount() == 0) {
// Currently empty, so we can accept any fluid.
int levelsInserted = Math.min(maxLevelsInserted, insertContent.maxLevel);
if (levelsInserted > 0) {
updateLevel(insertContent, levelsInserted, transaction);
}
return levelsInserted * insertContent.amountPerLevel;
}
CauldronFluidContent currentContent = getCurrentContent();
if (fluidVariant.isOf(currentContent.fluid)) {
// Otherwise we can only accept the same fluid as the current one.
int currentLevel = currentContent.currentLevel(createSnapshot());
int levelsInserted = Math.min(maxLevelsInserted, currentContent.maxLevel - currentLevel);
if (levelsInserted > 0) {
updateLevel(currentContent, currentLevel + levelsInserted, transaction);
}
return levelsInserted * currentContent.amountPerLevel;
}
}
return 0;
}
@Override
public long extract(FluidVariant fluidVariant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(fluidVariant, maxAmount);
CauldronFluidContent currentContent = getCurrentContent();
if (fluidVariant.isOf(currentContent.fluid)) {
int maxLevelsExtracted = Ints.saturatedCast(maxAmount / currentContent.amountPerLevel);
int currentLevel = currentContent.currentLevel(createSnapshot());
int levelsExtracted = Math.min(maxLevelsExtracted, currentLevel);
if (levelsExtracted > 0) {
if (levelsExtracted == currentLevel) {
// Fully extract -> back to empty cauldron
updateSnapshots(transaction);
location.world.setBlockState(location.pos, Blocks.CAULDRON.getDefaultState(), 0);
} else {
// Otherwise just decrease levels
updateLevel(currentContent, currentLevel - levelsExtracted, transaction);
}
}
return levelsExtracted * currentContent.amountPerLevel;
}
return 0;
}
@Override
public boolean isResourceBlank() {
return getResource().isBlank();
}
@Override
public FluidVariant getResource() {
return FluidVariant.of(getCurrentContent().fluid);
}
@Override
public long getAmount() {
CauldronFluidContent currentContent = getCurrentContent();
return currentContent.currentLevel(createSnapshot()) * currentContent.amountPerLevel;
}
@Override
public long getCapacity() {
CauldronFluidContent currentContent = getCurrentContent();
return currentContent.maxLevel * currentContent.amountPerLevel;
}
@Override
public BlockState createSnapshot() {
return location.world.getBlockState(location.pos);
}
@Override
public void readSnapshot(BlockState savedState) {
location.world.setBlockState(location.pos, savedState, 0);
}
@Override
public void onFinalCommit() {
BlockState state = createSnapshot();
BlockState originalState = lastReleasedSnapshot;
if (originalState != state) {
// Revert change
location.world.setBlockState(location.pos, originalState, 0);
// Then do the actual change with normal block updates
location.world.setBlockState(location.pos, state);
}
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.transfer.fluid;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
/**
* Implemented by fluids to cache the FluidVariant with a null tag inside the Fluid object directly.
*/
public interface FluidVariantCache {
FluidVariant fabric_getCachedFluidVariant();
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.transfer.fluid;
import java.util.Objects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import net.minecraft.fluid.Fluid;
import net.minecraft.fluid.Fluids;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.util.Identifier;
import net.minecraft.util.registry.Registry;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
public class FluidVariantImpl implements FluidVariant {
public static FluidVariant of(Fluid fluid, @Nullable NbtCompound nbt) {
Objects.requireNonNull(fluid, "Fluid may not be null.");
if (!fluid.isStill(fluid.getDefaultState()) && fluid != Fluids.EMPTY) {
// Note: the empty fluid is not still, that's why we check for it specifically.
throw new IllegalArgumentException("Fluid may not be flowing.");
}
if (nbt == null || fluid == Fluids.EMPTY) {
// Use the cached variant inside the fluid
return ((FluidVariantCache) fluid).fabric_getCachedFluidVariant();
} else {
// TODO explore caching fluid variants for non null tags.
return new FluidVariantImpl(fluid, nbt);
}
}
private static final Logger LOGGER = LogManager.getLogger("fabric-transfer-api-v1/fluid");
private final Fluid fluid;
private final @Nullable NbtCompound nbt;
private final int hashCode;
public FluidVariantImpl(Fluid fluid, NbtCompound nbt) {
this.fluid = fluid;
this.nbt = nbt == null ? null : nbt.copy(); // defensive copy
this.hashCode = Objects.hash(fluid, nbt);
}
@Override
public boolean isBlank() {
return fluid == Fluids.EMPTY;
}
@Override
public Fluid getObject() {
return fluid;
}
@Override
public @Nullable NbtCompound getNbt() {
return nbt;
}
@Override
public NbtCompound toNbt() {
NbtCompound result = new NbtCompound();
result.putString("fluid", Registry.FLUID.getId(fluid).toString());
if (nbt != null) {
result.put("tag", nbt.copy());
}
return result;
}
public static FluidVariant fromNbt(NbtCompound compound) {
try {
Fluid fluid = Registry.FLUID.get(new Identifier(compound.getString("fluid")));
NbtCompound nbt = compound.contains("tag") ? compound.getCompound("tag") : null;
return of(fluid, nbt);
} catch (RuntimeException runtimeException) {
LOGGER.debug("Tried to load an invalid FluidVariant from NBT: {}", compound, runtimeException);
return FluidVariant.blank();
}
}
@Override
public void toPacket(PacketByteBuf buf) {
if (isBlank()) {
buf.writeBoolean(false);
} else {
buf.writeBoolean(true);
buf.writeVarInt(Registry.FLUID.getRawId(fluid));
buf.writeNbt(nbt);
}
}
public static FluidVariant fromPacket(PacketByteBuf buf) {
if (!buf.readBoolean()) {
return FluidVariant.blank();
} else {
Fluid fluid = Registry.FLUID.get(buf.readVarInt());
NbtCompound nbt = buf.readNbt();
return of(fluid, nbt);
}
}
@Override
public String toString() {
return "FluidVariantImpl{fluid=" + fluid + ", tag=" + nbt + '}';
}
@Override
public boolean equals(Object o) {
// succeed fast with == check
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FluidVariantImpl fluidVariant = (FluidVariantImpl) o;
// fail fast with hash code
return hashCode == fluidVariant.hashCode && fluid == fluidVariant.fluid && nbtMatches(fluidVariant.nbt);
}
@Override
public int hashCode() {
return hashCode;
}
}

View file

@ -0,0 +1,216 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.transfer.transaction;
import java.util.ArrayList;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
public class TransactionManagerImpl {
public static final ThreadLocal<TransactionManagerImpl> MANAGERS = ThreadLocal.withInitial(TransactionManagerImpl::new);
private final Thread thread = Thread.currentThread();
private final ArrayList<TransactionImpl> stack = new ArrayList<>();
private final ArrayList<Transaction.OuterCloseCallback> outerCloseCallbacks = new ArrayList<>();
private int currentDepth = -1;
public boolean isOpen() {
return currentDepth > -1;
}
public Transaction openOuter() {
if (isOpen()) {
throw new IllegalStateException("An outer transaction is already active on this thread.");
}
return open();
}
/**
* Open a new transaction, outer or nested, without performing any state check.
*/
Transaction open() {
currentDepth++;
if (stack.size() == currentDepth) {
stack.add(new TransactionImpl(currentDepth));
}
TransactionImpl current = stack.get(currentDepth);
current.isOpen = true;
return current;
}
void validateCurrentThread() {
if (Thread.currentThread() != thread) {
String errorMessage = String.format(
"Attempted to access transaction state from thread %s, but this transaction is only valid on thread %s.",
Thread.currentThread().getName(),
thread.getName());
throw new IllegalStateException(errorMessage);
}
}
private class TransactionImpl implements Transaction {
final int nestingDepth;
final ArrayList<CloseCallback> closeCallbacks = new ArrayList<>();
// This may be false even when the transaction is not fully closed, to prevent callbacks calling other functions in an invalid state.
// It is reset to true in TransactionManagerImpl#open.
boolean isOpen = false;
TransactionImpl(int nestingDepth) {
this.nestingDepth = nestingDepth;
}
void validateCurrentTransaction() {
validateCurrentThread();
if (currentDepth == -1 || stack.get(currentDepth) != this) {
String errorMessage = String.format(
"Transaction function was called on a transaction with depth %d, but the current transaction has depth %d.",
nestingDepth,
currentDepth);
throw new IllegalStateException(errorMessage);
}
}
// Validate that this transaction is open.
private void validateOpen() {
if (!isOpen) {
throw new IllegalStateException("Transaction operation cannot be applied to a closed transaction.");
}
}
@Override
public Transaction openNested() {
validateCurrentTransaction();
validateOpen();
return open();
}
private void close(Result result) {
validateCurrentTransaction();
validateOpen();
// Block transaction operations
isOpen = false;
// Note: it is important that we don't let exceptions corrupt the global state of the transaction manager.
// That is why any callback has to run inside a try block.
RuntimeException closeException = null;
// Invoke callbacks in reverse order
for (int i = closeCallbacks.size()-1; i >= 0; i--) {
try {
closeCallbacks.get(i).onClose(this, result);
} catch (Exception exception) {
if (closeException == null) {
closeException = new RuntimeException("Encountered an exception while invoking a transaction close callback.", exception);
} else {
closeException.addSuppressed(exception);
}
}
}
closeCallbacks.clear();
if (currentDepth == 0) {
// Invoke outer close callbacks in reverse order
for (int i = outerCloseCallbacks.size() - 1; i >= 0; i--) {
try {
outerCloseCallbacks.get(i).afterOuterClose(result);
} catch (Exception exception) {
if (closeException == null) {
closeException = new RuntimeException("Encountered an exception while invoking a transaction outer close callback.", exception);
} else {
closeException.addSuppressed(exception);
}
}
}
outerCloseCallbacks.clear();
}
// Only this check will allow openOuter operations.
currentDepth--;
// Throw exception if necessary
if (closeException != null) {
throw closeException;
}
}
@Override
public void abort() {
close(Result.ABORTED);
}
@Override
public void commit() {
close(Result.COMMITTED);
}
@Override
public void close() {
if (isOpen() && isOpen) { // check that a transaction is open on this thread and that this transaction is open.
abort();
}
}
@Override
public int nestingDepth() {
validateCurrentThread();
return nestingDepth;
}
@Override
public Transaction getOpenTransaction(int nestingDepth) {
validateCurrentThread();
if (nestingDepth < 0) {
throw new IndexOutOfBoundsException("Nesting depth may not be negative.");
}
if (nestingDepth > currentDepth) {
throw new IndexOutOfBoundsException("There is no open transaction for nesting depth " + nestingDepth);
}
TransactionImpl transaction = stack.get(nestingDepth);
transaction.validateOpen();
return transaction;
}
@Override
public void addCloseCallback(CloseCallback closeCallback) {
validateCurrentThread();
validateOpen();
closeCallbacks.add(closeCallback);
}
@Override
public void addOuterCloseCallback(OuterCloseCallback outerCloseCallback) {
validateCurrentThread();
// Note: we don't call validateOpen() because this transaction may not be open if this is called during a CloseCallback.
// We rely on a currentDepth check instead, as the depth is only set to -1 at the very end of close(Result).
if (currentDepth == -1) {
throw new IllegalStateException("There is no open transaction on this thread.");
}
outerCloseCallbacks.add(outerCloseCallback);
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.transfer;
import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.fluid.Fluid;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.impl.transfer.fluid.FluidVariantImpl;
import net.fabricmc.fabric.impl.transfer.fluid.FluidVariantCache;
/**
* Cache the FluidVariant with a null tag inside each Fluid directly.
*/
@Mixin(Fluid.class)
@SuppressWarnings("unused")
public class FluidMixin implements FluidVariantCache {
@SuppressWarnings("ConstantConditions")
private final FluidVariant cachedFluidVariant = new FluidVariantImpl((Fluid) (Object) this, null);
@Override
public FluidVariant fabric_getCachedFluidVariant() {
return cachedFluidVariant;
}
}

View file

@ -0,0 +1,8 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.transfer",
"compatibilityLevel": "JAVA_8",
"mixins": [
"FluidMixin"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,30 @@
{
"schemaVersion": 1,
"id": "fabric-transfer-api-v1",
"name": "Fabric Transfer API (v1)",
"version": "${version}",
"environment": "*",
"license": "Apache-2.0",
"icon": "assets/fabric-api-lookup-api-v1/icon.png",
"contact": {
"homepage": "https://fabricmc.net",
"irc": "irc://irc.esper.net:6667/fabric",
"issues": "https://github.com/FabricMC/fabric/issues",
"sources": "https://github.com/FabricMC/fabric"
},
"authors": [
"FabricMC"
],
"depends": {
"fabricloader": ">=0.9.2",
"fabric-api-lookup-api-v1": "*",
"fabric-rendering-fluids-v1": "*"
},
"description": "A common API for the transfer of fluids and other game resources.",
"mixins": [
"fabric-transfer-api-v1.mixins.json"
],
"custom": {
"fabric-api:module-lifecycle": "experimental"
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.transfer.fluid;
import java.util.Iterator;
import net.minecraft.fluid.Fluids;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.fabric.api.transfer.v1.storage.base.ExtractionOnlyStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleViewIterator;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
public class CreativeFluidStorage implements ExtractionOnlyStorage<FluidVariant>, StorageView<FluidVariant> {
public static final CreativeFluidStorage WATER = new CreativeFluidStorage(FluidVariant.of(Fluids.WATER));
public static final CreativeFluidStorage LAVA = new CreativeFluidStorage(FluidVariant.of(Fluids.LAVA));
private final FluidVariant infiniteFluid;
private CreativeFluidStorage(FluidVariant infiniteFluid) {
this.infiniteFluid = infiniteFluid;
}
@Override
public boolean isResourceBlank() {
return infiniteFluid.isBlank();
}
@Override
public FluidVariant getResource() {
return infiniteFluid;
}
@Override
public long getAmount() {
return Long.MAX_VALUE;
}
@Override
public long getCapacity() {
return getAmount();
}
@Override
public long extract(FluidVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
if (resource.equals(infiniteFluid)) {
return maxAmount;
} else {
return 0;
}
}
@Override
public Iterator<StorageView<FluidVariant>> iterator(TransactionContext transaction) {
return SingleViewIterator.create(this, transaction);
}
@Override
public long getVersion() {
return 0;
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.transfer.fluid;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.item.ItemUsageContext;
import net.minecraft.util.ActionResult;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
public class ExtractStickItem extends Item {
public ExtractStickItem() {
super(new Settings().group(ItemGroup.MISC));
}
@Override
public ActionResult useOnBlock(ItemUsageContext context) {
Storage<FluidVariant> storage = FluidStorage.SIDED.find(context.getWorld(), context.getBlockPos(), context.getSide());
try (Transaction transaction = Transaction.openOuter()) {
// Find something to extract
FluidVariant stored = StorageUtil.findExtractableResource(storage, transaction);
if (stored == null) return ActionResult.PASS;
// By now, storage can't be null :P
long extracted = storage.extract(stored, FluidConstants.BUCKET, transaction);
// If sneaking, we require exact extraction (can be tested on cauldrons)
boolean requireExact = context.getPlayer() != null && context.getPlayer().isSneaking();
if (!requireExact || extracted == FluidConstants.BUCKET) {
transaction.commit();
return ActionResult.success(context.getWorld().isClient());
}
}
return ActionResult.FAIL;
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.transfer.fluid;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.Block;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.Material;
import net.minecraft.block.ShapeContext;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.shape.VoxelShape;
import net.minecraft.util.shape.VoxelShapes;
import net.minecraft.world.BlockView;
import net.minecraft.world.World;
public class FluidChuteBlock extends Block implements BlockEntityProvider {
public FluidChuteBlock() {
super(Settings.of(Material.METAL));
}
private static final VoxelShape SHAPE = VoxelShapes.cuboid(
3 / 16f, 0, 3 / 16f, 13 / 16f, 1, 13 / 16f
);
@Override
public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new FluidChuteBlockEntity(pos, state);
}
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return (w, p, s, be) -> ((FluidChuteBlockEntity) be).tick();
}
@Override
public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return SHAPE;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.transfer.fluid;
import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
public class FluidChuteBlockEntity extends BlockEntity {
private int tickCounter = 0;
public FluidChuteBlockEntity(BlockPos pos, BlockState state) {
super(FluidTransferTest.FLUID_CHUTE_TYPE, pos, state);
}
@SuppressWarnings("ConstantConditions")
public void tick() {
if (!world.isClient() && tickCounter++ % 20 == 0) {
Storage<FluidVariant> top = FluidStorage.SIDED.find(world, pos.offset(Direction.UP), Direction.DOWN);
Storage<FluidVariant> bottom = FluidStorage.SIDED.find(world, pos.offset(Direction.DOWN), Direction.UP);
if (top != null && bottom != null) {
StorageUtil.move(top, bottom, fluid -> true, FluidConstants.BUCKET, null);
}
}
}
}

View file

@ -0,0 +1,242 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.transfer.fluid;
import static net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants.BUCKET;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.Material;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.fluid.Fluids;
import net.minecraft.item.BlockItem;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.Identifier;
import net.minecraft.util.registry.Registry;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage;
import net.fabricmc.fabric.api.transfer.v1.fluid.base.SingleFluidStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
public class FluidTransferTest implements ModInitializer {
public static final String MOD_ID = "fabric-transfer-api-v1-testmod";
private static final Block INFINITE_WATER_SOURCE = new Block(AbstractBlock.Settings.of(Material.METAL));
private static final Block INFINITE_LAVA_SOURCE = new Block(AbstractBlock.Settings.of(Material.METAL));
private static final Block FLUID_CHUTE = new FluidChuteBlock();
private static final Item EXTRACT_STICK = new ExtractStickItem();
public static BlockEntityType<FluidChuteBlockEntity> FLUID_CHUTE_TYPE;
@Override
public void onInitialize() {
registerBlock(INFINITE_WATER_SOURCE, "infinite_water_source");
registerBlock(INFINITE_LAVA_SOURCE, "infinite_lava_source");
registerBlock(FLUID_CHUTE, "fluid_chute");
Registry.register(Registry.ITEM, new Identifier(MOD_ID, "extract_stick"), EXTRACT_STICK);
FLUID_CHUTE_TYPE = FabricBlockEntityTypeBuilder.create(FluidChuteBlockEntity::new, FLUID_CHUTE).build();
Registry.register(Registry.BLOCK_ENTITY_TYPE, new Identifier(MOD_ID, "fluid_chute"), FLUID_CHUTE_TYPE);
FluidStorage.SIDED.registerForBlocks((world, pos, state, be, direction) -> CreativeFluidStorage.WATER, INFINITE_WATER_SOURCE);
FluidStorage.SIDED.registerForBlocks((world, pos, state, be, direction) -> CreativeFluidStorage.LAVA, INFINITE_LAVA_SOURCE);
testFluidStorage();
testTransactionExceptions();
}
private static void registerBlock(Block block, String name) {
Identifier id = new Identifier(MOD_ID, name);
Registry.register(Registry.BLOCK, id, block);
Registry.register(Registry.ITEM, id, new BlockItem(block, new Item.Settings().group(ItemGroup.MISC)));
}
private static final FluidVariant TAGGED_WATER, TAGGED_WATER_2, WATER, LAVA;
private static int markDirtyCount = 0;
private static SingleFluidStorage createWaterStorage() {
return new SingleFluidStorage() {
@Override
protected long getCapacity(FluidVariant fluidVariant) {
return BUCKET * 2;
}
@Override
protected boolean canInsert(FluidVariant fluidVariant) {
return fluidVariant.isOf(Fluids.WATER);
}
@Override
protected void markDirty() {
markDirtyCount++;
}
};
}
static {
NbtCompound tag = new NbtCompound();
tag.putInt("test", 1);
TAGGED_WATER = FluidVariant.of(Fluids.WATER, tag);
TAGGED_WATER_2 = FluidVariant.of(Fluids.WATER, tag);
WATER = FluidVariant.of(Fluids.WATER);
LAVA = FluidVariant.of(Fluids.LAVA);
}
private static void testFluidStorage() {
SingleFluidStorage waterStorage = createWaterStorage();
// Test content
if (!waterStorage.isResourceBlank()) throw new AssertionError("Should have been blank");
// Test some insertions
try (Transaction tx = Transaction.openOuter()) {
// Should not allow lava (canInsert returns false)
if (waterStorage.insert(LAVA, BUCKET, tx) != 0) throw new AssertionError("Lava inserted");
// Should allow insert
if (waterStorage.insert(TAGGED_WATER, BUCKET, tx) != BUCKET) throw new AssertionError("Tagged water insert 1 failed");
// Variants are different, should not allow insert
if (waterStorage.insert(WATER, BUCKET, tx) != 0) throw new AssertionError("Water inserted");
// Should allow insert again even if the variant is different cause they are equal
if (waterStorage.insert(TAGGED_WATER_2, BUCKET, tx) != BUCKET) throw new AssertionError("Tagged water insert 2 failed");
// Should not allow further insertion because the storage is full
if (waterStorage.insert(TAGGED_WATER, BUCKET, tx) != 0) throw new AssertionError("Storage full, yet something was inserted");
// Should allow extraction
if (waterStorage.extract(TAGGED_WATER_2, BUCKET, tx) != BUCKET) throw new AssertionError("Extraction failed");
// Re-insert
if (waterStorage.insert(TAGGED_WATER_2, BUCKET, tx) != BUCKET) throw new AssertionError("Tagged water insert 3 failed");
// Test contents
if (waterStorage.getAmount() != BUCKET * 2 || !waterStorage.getResource().equals(TAGGED_WATER_2)) throw new AssertionError("Contents are wrong");
// No commit -> will abort
}
// Test content again to make sure the rollback worked as expected
if (!waterStorage.isResourceBlank()) throw new AssertionError("Should have been blank");
// Test highly nested commit
try (Transaction tx = Transaction.openOuter()) {
if (waterStorage.getAmount() != 0) throw new AssertionError("Initial amount is wrong");
if (waterStorage.insert(WATER, BUCKET, tx) != BUCKET) throw new AssertionError("Water insertion failed");
try (Transaction nested1 = tx.openNested()) {
try (Transaction nested2 = nested1.openNested()) {
if (waterStorage.insert(WATER, BUCKET, nested2) != BUCKET) throw new AssertionError("Nested insertion failed");
if (waterStorage.getAmount() != 2 * BUCKET) throw new AssertionError("Two buckets have been inserted");
nested2.commit();
}
if (waterStorage.getAmount() != 2 * BUCKET) throw new AssertionError("Nested no 1 was committed, so we should still have two buckets");
nested1.commit();
}
if (waterStorage.getAmount() != 2 * BUCKET) throw new AssertionError("Nested no 1 was committed, so we should still have two buckets");
}
if (waterStorage.getAmount() != 0) throw new AssertionError("Amount should have been reverted to zero");
// Test nested commit to make sure it behaves as expected
// Without outer commit
insertWaterWithNesting(waterStorage, false);
if (waterStorage.getAmount() != 0) throw new AssertionError("Amount should have been reverted to zero");
if (markDirtyCount != 0) throw new AssertionError("Nothing should have called markDirty() yet (no outer commit)");
// With outer commit
insertWaterWithNesting(waterStorage, true);
if (waterStorage.getAmount() != 2 * BUCKET) throw new AssertionError("Outer was committed, so we should still have two buckets");
if (markDirtyCount != 1) throw new AssertionError("markDirty() should have been called exactyl once.");
}
private static void insertWaterWithNesting(SingleFluidStorage waterStorage, boolean doOuterCommit) {
try (Transaction tx = Transaction.openOuter()) {
if (waterStorage.getAmount() != 0) throw new AssertionError("Initial amount is wrong");
if (waterStorage.insert(WATER, BUCKET, tx) != BUCKET) throw new AssertionError("Water insertion failed");
try (Transaction nested = tx.openNested()) {
if (waterStorage.insert(WATER, BUCKET, nested) != BUCKET) throw new AssertionError("Nested insertion failed");
if (waterStorage.getAmount() != 2 * BUCKET) throw new AssertionError("Two buckets have been inserted");
nested.commit();
}
if (waterStorage.getAmount() != 2 * BUCKET) throw new AssertionError("Nested was committed, so we should still have two buckets");
if (doOuterCommit) {
tx.commit();
}
}
}
private static int callbacksInvoked = 0;
/**
* Make sure that transaction global state stays valid in case of exceptions.
*/
private static void testTransactionExceptions() {
// Test exception inside the try.
ensureException(() -> {
try (Transaction tx = Transaction.openOuter()) {
tx.addCloseCallback((t, result) -> {
callbacksInvoked++; throw new RuntimeException("Close.");
});
throw new RuntimeException("Inside try.");
}
}, "Exception should have propagated through the transaction.");
if (callbacksInvoked != 1) throw new AssertionError("Callback should have been invoked.");
// Test exception inside the close.
callbacksInvoked = 0;
ensureException(() -> {
try (Transaction tx = Transaction.openOuter()) {
tx.addCloseCallback((t, result) -> {
callbacksInvoked++; throw new RuntimeException("Close 1.");
});
tx.addCloseCallback((t, result) -> {
callbacksInvoked++; throw new RuntimeException("Close 2.");
});
tx.addOuterCloseCallback(result -> {
callbacksInvoked++; throw new RuntimeException("Outer close 1.");
});
tx.addOuterCloseCallback(result -> {
callbacksInvoked++; throw new RuntimeException("Outer close 2.");
});
}
}, "Exceptions in close callbacks should be propagated through the transaction.");
if (callbacksInvoked != 4) throw new AssertionError("All 4 callbacks should have been invoked, only so many were: " + callbacksInvoked);
// Test that transaction state is still OK after these exceptions.
try (Transaction tx = Transaction.openOuter()) {
tx.commit();
}
}
private static void ensureException(Runnable runnable, String message) {
boolean failed = false;
try {
runnable.run();
} catch (Throwable t) {
failed = true;
}
if (!failed) {
throw new AssertionError(message);
}
}
}

View file

@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "fabric-transfer-api-v1-testmod:block/fluid_chute"
}
}
}

View file

@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "fabric-transfer-api-v1-testmod:block/infinite_lava_source"
}
}
}

View file

@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "fabric-transfer-api-v1-testmod:block/infinite_water_source"
}
}
}

View file

@ -0,0 +1,21 @@
{
"credit": "Made with Blockbench",
"textures": {
"0": "fabric-transfer-api-v1-testmod:block/fluid_chute",
"particle": "fabric-transfer-api-v1-testmod:block/fluid_chute"
},
"elements": [
{
"from": [3, 0, 3],
"to": [13, 16, 13],
"faces": {
"north": {"uv": [0, 0, 10, 16], "texture": "#0"},
"east": {"uv": [0, 0, 10, 16], "texture": "#0"},
"south": {"uv": [0, 0, 10, 16], "texture": "#0"},
"west": {"uv": [0, 0, 10, 16], "texture": "#0"},
"up": {"uv": [6, 0, 16, 10], "texture": "#0"},
"down": {"uv": [6, 0, 16, 10], "texture": "#0"}
}
}
]
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "fabric-transfer-api-v1-testmod:block/infinite_lava_source"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "fabric-transfer-api-v1-testmod:block/infinite_water_source"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/handheld",
"textures": {
"layer0": "fabric-transfer-api-v1-testmod:item/extract_stick"
}
}

View file

@ -0,0 +1,3 @@
{
"parent": "fabric-transfer-api-v1-testmod:block/fluid_chute"
}

View file

@ -0,0 +1,3 @@
{
"parent": "fabric-transfer-api-v1-testmod:block/infinite_lava_source"
}

View file

@ -0,0 +1,3 @@
{
"parent": "fabric-transfer-api-v1-testmod:block/infinite_water_source"
}

View file

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"id": "fabric-transfer-api-v1-testmod",
"name": "Fabric Transfer API (v1) Test Mod",
"version": "1.0.0",
"environment": "*",
"license": "Apache-2.0",
"depends": {
"fabric-transfer-api-v1": "*"
},
"entrypoints": {
"main": [
"net.fabricmc.fabric.test.transfer.fluid.FluidTransferTest"
]
}
}

View file

@ -55,3 +55,4 @@ include 'fabric-structure-api-v1'
include 'fabric-tag-extensions-v0'
include 'fabric-textures-v0'
include 'fabric-tool-attribute-api-v1'
include 'fabric-transfer-api-v1'