Fabric Transfer API: item transfer and fluid-containing items. ()

* Add item and "fluid item" APIs

* Rework ContainerItemContext javadoc

* Rework the Inventory wrapper API

* Cleanup inventory wrapper implementation, add < 64 max stack count test, separate tests better

* Fix Inventory wrapper not limiting the stack count correctly (thanks @lilybeevee!)

* Rewrite inventory wrapper, add SingleStackStorage base implementation

* Composters

* SingleStackStorage adjustements

* Bump version

* Move icon to correct location. Closes 

* Bump version

* Remove composter implementation (it's broken), slight renames

* Fix SidedInventory extract

* Bump version

* Don't use MAVEN_USERNAME if it's not specified

* Add comparator output, add missing markDirty calls, fix tests

* Bump version

* Add SingleVariantStorage, deprecate SingleFluidStorage, definalize a few things, make sure markDirty() is only called once per successful outer transaction in inventory wrappers

* Add composter support

* Move EmptyFluidView to BlankVariantView, update README and package-info

* Bump version

* Key -> variant

* Add Transaction#openNested(@Nullable TransactionContext)

* Add SingleSlotContainerItemContext

* Bump prerelease version

* Remove useless comment

* Remove ContainerItemContext#getWorld

* Bump prerelease version

* Add StorageUtil#findExtractableContent and ContainerItemContext#withInitial

* Bump prerelease version
This commit is contained in:
Technici4n 2021-08-17 20:08:09 +02:00 committed by GitHub
parent f3747de3d0
commit 0d7a4ee070
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 3564 additions and 45 deletions
build.gradle
fabric-api-lookup-api-v1
build.gradle
src/main/java/net/fabricmc/fabric
fabric-transfer-api-v1

View file

@ -311,9 +311,11 @@ void setupRepositories(RepositoryHandler repositories) {
if (ENV.MAVEN_URL) {
repositories.maven {
url ENV.MAVEN_URL
credentials {
username ENV.MAVEN_USERNAME
password ENV.MAVEN_PASSWORD
if (ENV.MAVEN_USERNAME) {
credentials {
username ENV.MAVEN_USERNAME
password ENV.MAVEN_PASSWORD
}
}
}
}

View file

@ -1,5 +1,5 @@
archivesBaseName = "fabric-api-lookup-api-v1"
version = getSubprojectVersion(project, "1.2.0")
version = getSubprojectVersion(project, "1.3.0")
moduleDependencies(project, [
'fabric-api-base',

View file

@ -227,6 +227,13 @@ public interface BlockApiLookup<A, C> {
*/
Class<C> contextClass();
/**
* Return the provider for the passed block (registered with one of the {@code register} functions), or null if none was registered (yet).
* Queries should go through {@link #find}, only use this to inspect registered providers!
*/
@Nullable
BlockApiProvider<A, C> getProvider(Block block);
@FunctionalInterface
interface BlockApiProvider<A, C> {
/**

View file

@ -19,6 +19,7 @@ package net.fabricmc.fabric.api.lookup.v1.item;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.Item;
import net.minecraft.item.ItemConvertible;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier;
@ -154,6 +155,13 @@ public interface ItemApiLookup<A, C> {
*/
Class<C> contextClass();
/**
* Return the provider for the passed item (registered with one of the {@code register} functions), or null if none was registered (yet).
* Queries should go through {@link #find}, only use this to inspect registered providers!
*/
@Nullable
ItemApiProvider<A, C> getProvider(Item item);
@FunctionalInterface
interface ItemApiProvider<A, C> {
/**

View file

@ -184,6 +184,7 @@ public final class BlockApiLookupImpl<A, C> implements BlockApiLookup<A, C> {
return contextClass;
}
@Override
@Nullable
public BlockApiProvider<A, C> getProvider(Block block) {
return providerMap.get(block);

View file

@ -133,4 +133,10 @@ public class ItemApiLookupImpl<A, C> implements ItemApiLookup<A, C> {
public Class<C> contextClass() {
return contextClass;
}
@Override
@Nullable
public ItemApiProvider<A, C> getProvider(Item item) {
return providerMap.get(item);
}
}

View file

@ -17,15 +17,15 @@ 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>`.
Implementors of inventories with a fixed number of "slots" or "tanks" can use
[`SingleVariantStorage`](src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleStorage.java),
and combine them with `CombinedStorage`.
## 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.
@ -34,3 +34,17 @@ Client-side [Fluid variant rendering](src/main/java/net/fabricmc/fabric/api/tran
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).
## Item transfer
A `Storage<ItemVariant>` is any object that can store items.
Instances can be accessed through the API lookup defined in [`ItemStorage`](src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java).
The lookup already provides compatibility with vanilla inventories, however it may sometimes be interesting to use
[`InventoryStorage`](src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java) or
[`PlayerInventoryStorage`](src/main/java/net/fabricmc/fabric/api/transfer/v1/item/PlayerInventoryStorage.java) when interaction with
`Inventory`-based APIs is required.
## `ContainerItemContext`
[`ContainerItemContext`](src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java) is a context designed for `ItemApiLookup` queries
that allows the returned APIs to interact with the containing inventory.
Notably, it is used by the `FluidStorage.ITEM` lookup for fluid-containing items.

View file

@ -1,5 +1,5 @@
archivesBaseName = "fabric-transfer-api-v1"
version = getSubprojectVersion(project, "1.0.0")
version = getSubprojectVersion(project, "1.1.0-pre.09")
moduleDependencies(project, [
'fabric-api-base',

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.api.transfer.v1.context;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.util.Hand;
import net.fabricmc.fabric.api.lookup.v1.item.ItemApiLookup;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.item.PlayerInventoryStorage;
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.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import net.fabricmc.fabric.impl.transfer.context.InitialContentsContainerItemContext;
import net.fabricmc.fabric.impl.transfer.context.PlayerContainerItemContext;
import net.fabricmc.fabric.impl.transfer.context.SingleSlotContainerItemContext;
/**
* A context that allows an item-queried {@link Storage} implementation to interact with its containing inventory,
* such as a player inventory or an emptying or filling machine.
* For example, it is what allows the {@code Storage<FluidVariant>} of a water bucket to replace the full bucket by an empty bucket
* on extraction.
* Such items that contain resources are often referred to as "container items".
*
* <p>When an {@linkplain ItemApiLookup item API} requires a {@code ContainerItemContext} as context,
* it will generally be suitable to obtain a context instance with {@link #ofPlayerHand} or {@link #ofPlayerCursor},
* and then use {@link #find} to query an API instance.
*
* <p>When water is extracted from the {@code Storage} of a water bucket, this is how it interacts with the context:
* <ul>
* <li>The first step is to remove one water bucket item from the current slot,
* that is the slot that contains the water bucket.</li>
* <li>The second step is to try to add one empty bucket item to the current slot, at the same position.</li>
* <li>If that fails, the third step is to add the empty bucket item somewhere else in the inventory.</li>
* <li>The water extraction can only proceed if both step 1, and step 2 or 3, succeed.</li>
* </ul>
* Before attempting to change the current item, the {@code Storage} implementation must of course check that
* the item in the current slot is still a water bucket.
*
* <p>A {@code ContainerItemContext} allows these operations to be performed, thanks to the following parts:
* <ul>
* <li>{@linkplain #getMainSlot The main slot} or current slot of the context, containing the item the API was queried for initially.
* In the example above, this is the slot containing the water bucket, used for steps 1 and 2.</li>
* <li>{@linkplain #insertOverflow An overflow insertion function}, that can be used to insert items into the context's inventory
* when insertion into a specific slot fails. In our example above, this is the function used for step 3.</li>
* <li>The context may also contain additional slots, accessible through {@link #getAdditionalSlots}.</li>
* </ul>
*
* <p>Implementors of item APIs can freely use these methods, but most will generally want to use the following convenience methods instead:
* <ul>
* <li>Query which variant is currently in the main slot through {@link #getItemVariant}.
* <b>It is important to check this before any operation, to make sure the item variant hasn't changed since the query.</b></li>
* <li>Query how much of the (non-blank) variant is in the inventory through {@link #getAmount}.</li>
* <li>Extract some items from the main slot with {@link #extract}. In the water bucket example, this can be used for step 1.</li>
* <li>Insert some items, either into the main slot if possible or the rest of the inventory otherwise, with {@link #insert}.
* In the water bucket example, this can be used for steps 2 and 3.</li>
* <li>Exchange some of the current variant with another variant through {@link #exchange}.
* In the water bucket example, this function can be used to combine steps 1, 2 and 3.</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 interface ContainerItemContext {
/**
* Return a context for the passed player's hand. This is recommended for item use interactions.
*/
static ContainerItemContext ofPlayerHand(PlayerEntity player, Hand hand) {
return new PlayerContainerItemContext(player, hand);
}
/**
* Return a context for the passed player's cursor slot. This is recommended for screen handler click interactions.
*/
static ContainerItemContext ofPlayerCursor(PlayerEntity player, ScreenHandler screenHandler) {
return ofPlayerSlot(player, PlayerInventoryStorage.getCursorStorage(screenHandler));
}
/**
* Return a context for a slot, with the passed player as fallback.
*/
static ContainerItemContext ofPlayerSlot(PlayerEntity player, SingleSlotStorage<ItemVariant> slot) {
return new PlayerContainerItemContext(player, slot);
}
/**
* Return a context for a single slot, with no fallback.
*
* @param slot The main slot of the context.
*/
static ContainerItemContext ofSingleSlot(SingleSlotStorage<ItemVariant> slot) {
return new SingleSlotContainerItemContext(slot);
}
/**
* Return a context that can accept anything, and will accept (and destroy) any overflow items, with some initial content.
* This can typically be used to check if a stack provides an API, or simulate operations on the returned API,
* for example to simulate how much fluid could be extracted from the stack.
*
* <p>Note that the stack can never be mutated by this function: its contents are copied directly.
*/
static ContainerItemContext withInitial(ItemStack initialContent) {
return withInitial(ItemVariant.of(initialContent), initialContent.getCount());
}
/**
* Return a context that can accept anything, and will accept (and destroy) any overflow items, with some initial variant and amount.
* This can typically be used to check if a variant provides an API, or simulate operations on the returned API,
* for example to simulate how much fluid could be extracted from the variant and amount.
*/
static ContainerItemContext withInitial(ItemVariant initialVariant, long initialAmount) {
StoragePreconditions.notBlankNotNegative(initialVariant, initialAmount);
return new InitialContentsContainerItemContext(initialVariant, initialAmount);
}
/**
* Try to find an API instance for the passed lookup and return it, or {@code null} if there is none.
* The API is queried for the current variant, if it's not blank.
*
* @see ItemApiLookup#find
*/
@Nullable
default <A> A find(ItemApiLookup<A, ContainerItemContext> lookup) {
return getItemVariant().isBlank() ? null : lookup.find(getItemVariant().toStack(), this);
}
/**
* Return the current item variant of this context, that is the variant in the slot of the context.
* If the result is non blank, {@link #getAmount} should be
*/
default ItemVariant getItemVariant() {
return getMainSlot().getResource();
}
/**
* Return the current amount of {@link #getItemVariant()} in the slot of the context.
*
* @throws IllegalStateException If {@linkplain #getItemVariant() the current variant} is blank.
*/
default long getAmount() {
if (getItemVariant().isBlank()) {
throw new IllegalStateException("Amount may not be queried when the current item variant is blank.");
}
return getMainSlot().getAmount();
}
/**
* Try to insert some items into this context, prioritizing the main slot over the rest of the inventory.
*
* @see Storage#insert
*/
default long insert(ItemVariant itemVariant, long maxAmount, TransactionContext transaction) {
// Main slot first
long mainInserted = getMainSlot().insert(itemVariant, maxAmount, transaction);
// Overflow second
long overflowInserted = insertOverflow(itemVariant, maxAmount - mainInserted, transaction);
return mainInserted + overflowInserted;
}
/**
* Try to extract some items from this context's main slot.
*
* @see Storage#extract
*/
default long extract(ItemVariant itemVariant, long maxAmount, TransactionContext transaction) {
return getMainSlot().extract(itemVariant, maxAmount, transaction);
}
/**
* Try to exchange as many items as possible of {@linkplain #getItemVariant() the current variant} with another variant.
* That is, extract the old variant, and insert the same amount of the new variant instead.
*
* @param newVariant The variant of the items after the conversion. May not be blank.
* @param maxAmount The maximum amount of items to convert. 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 transformed.
*/
default long exchange(ItemVariant newVariant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(newVariant, maxAmount);
try (Transaction nested = transaction.openNested()) {
long extracted = extract(getItemVariant(), maxAmount, nested);
if (insert(newVariant, extracted, nested) == extracted) {
nested.commit();
return extracted;
}
}
return 0;
}
/**
* Return the main slot of this context.
*/
SingleSlotStorage<ItemVariant> getMainSlot();
/**
* Try to insert items into this context, without prioritizing a specific slot, similar to {@link PlayerInventory#offerOrDrop}.
* This should be used for insertion after insertion into the main slot failed.
* {@link #insert} can be used to insert into the main slot first, then send any overflow through this function.
*
* @see Storage#insert
*/
long insertOverflow(ItemVariant itemVariant, long maxAmount, TransactionContext transactionContext);
/**
* Get additional slots that may be available in this context.
* These may or may not include the main slot of this context, as it is not always practical to remove it from the list.
*
* @return An unmodifiable list containing additional slots of this context. If no additional slot is available, the list is empty.
*/
List<SingleSlotStorage<ItemVariant>> getAdditionalSlots();
}

View file

@ -17,13 +17,31 @@
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.item.BucketItem;
import net.minecraft.item.Item;
import net.minecraft.item.Items;
import net.minecraft.item.ItemStack;
import net.minecraft.potion.PotionUtil;
import net.minecraft.potion.Potions;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup;
import net.fabricmc.fabric.api.lookup.v1.item.ItemApiLookup;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
import net.fabricmc.fabric.api.transfer.v1.fluid.base.EmptyItemFluidStorage;
import net.fabricmc.fabric.api.transfer.v1.fluid.base.FullItemFluidStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.impl.transfer.fluid.EmptyBucketStorage;
import net.fabricmc.fabric.impl.transfer.fluid.CombinedProvidersImpl;
import net.fabricmc.fabric.impl.transfer.fluid.WaterPotionStorage;
import net.fabricmc.fabric.mixin.transfer.BucketItemAccessor;
/**
* Access to {@link Storage Storage&lt;FluidVariant&gt;} instances.
@ -49,11 +67,97 @@ public final class FluidStorage {
public static final BlockApiLookup<Storage<FluidVariant>, Direction> SIDED =
BlockApiLookup.get(new Identifier("fabric:sided_fluid_storage"), Storage.asClass(), Direction.class);
/**
* Item access to fluid variant storages.
* Querying should happen through {@link ContainerItemContext#find}.
*
* <p>Fluid amounts are always expressed in {@linkplain FluidConstants droplets}.
* By default, Fabric API only registers storage support for buckets that have a 1:1 mapping to their fluid, and for water potions.
*
* <p>{@link #combinedItemApiProvider} and {@link #GENERAL_COMBINED_PROVIDER} should be used for API provider registration
* when multiple mods may want to offer a storage for the same item.
*
* <p>Base implementations are provided: {@link EmptyItemFluidStorage} and {@link FullItemFluidStorage}.
*
* <p>This may be queried both client-side and server-side.
* Returned APIs should behave the same regardless of the logical side.
*/
public static final ItemApiLookup<Storage<FluidVariant>, ContainerItemContext> ITEM =
ItemApiLookup.get(new Identifier("fabric:fluid_storage"), Storage.asClass(), ContainerItemContext.class);
/**
* Get or create and register a {@link CombinedItemApiProvider} event for the passed item.
* Allows multiple API providers to provide a {@code Storage<FluidVariant>} implementation for the same item.
*
* <p>When the item is queried for an API through {@link #ITEM}, all the providers registered through the event will be invoked.
* All non-null {@code Storage<FluidVariant>} instances returned by the providers will be combined in a single storage,
* that will be the final result of the query, or {@code null} if no storage is offered by the event handlers.
*
* <p>This is appropriate to use when multiple mods could wish to expose the Fluid API for some items,
* for example when dealing with items added by the base Minecraft game such as buckets or empty bottles.
* A typical usage example is a mod adding support for filling empty bottles with a honey fluid:
* Fabric API already registers a storage for empty bottles to allow filling them with water through the event,
* and a mod can register an event handler that will attach a second storage allowing empty bottles to be filled with its honey fluid.
*
* @throws IllegalStateException If an incompatible provider is already registered for the item.
*/
public static Event<CombinedItemApiProvider> combinedItemApiProvider(Item item) {
return CombinedProvidersImpl.getOrCreateItemEvent(item);
}
/**
* Allows multiple API providers to return {@code Storage<FluidVariant>} implementations for some items.
* {@link #combinedItemApiProvider} is per-item while this one is queried for all items, hence the "general" name.
*
* <p>Implementation note: This event is invoked both through an API Lookup fallback, and by the {@code combinedItemApiProvider} events.
* This means that per-item combined providers registered through {@code combinedItemApiProvider} DO NOT prevent these general providers from running,
* however regular providers registered through {@code ItemApiLookup#register...} that return a non-null API instance DO prevent it.
*/
public static Event<CombinedItemApiProvider> GENERAL_COMBINED_PROVIDER = CombinedProvidersImpl.createEvent(false);
@FunctionalInterface
public interface CombinedItemApiProvider {
/**
* Return a {@code Storage<FluidVariant>} if available in the given context, or {@code null} otherwise.
* The current item variant can be {@linkplain ContainerItemContext#getItemVariant() retrieved from the context}.
*/
@Nullable
Storage<FluidVariant> find(ContainerItemContext context);
}
private FluidStorage() {
}
static {
// Initialize vanilla cauldron wrappers
CauldronFluidContent.getForFluid(Fluids.WATER);
// Register combined fallback
FluidStorage.ITEM.registerFallback((stack, context) -> GENERAL_COMBINED_PROVIDER.invoker().find(context));
// Register empty bucket storage
combinedItemApiProvider(Items.BUCKET).register(EmptyBucketStorage::new);
// Register full bucket storage
GENERAL_COMBINED_PROVIDER.register(context -> {
if (context.getItemVariant().getItem() instanceof BucketItem bucketItem) {
Fluid bucketFluid = ((BucketItemAccessor) bucketItem).fabric_getFluid();
// Make sure the mapping is bidirectional.
if (bucketFluid != null && bucketFluid.getBucketItem() == bucketItem) {
return new FullItemFluidStorage(context, Items.BUCKET, FluidVariant.of(bucketFluid), FluidConstants.BUCKET);
}
}
return null;
});
// Register empty bottle storage, only water potion is supported!
combinedItemApiProvider(Items.GLASS_BOTTLE).register(context -> {
return new EmptyItemFluidStorage(context, emptyBottle -> {
ItemStack newStack = emptyBottle.toStack();
PotionUtil.setPotion(newStack, Potions.WATER);
return ItemVariant.of(Items.POTION, newStack.getTag());
}, Fluids.WATER, FluidConstants.BOTTLE);
});
// Register water potion storage
combinedItemApiProvider(Items.POTION).register(WaterPotionStorage::find);
}
}

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.fluid.base;
import java.util.Iterator;
import java.util.function.Function;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.fluid.Fluid;
import net.minecraft.item.Item;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
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.BlankVariantView;
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.TransactionContext;
/**
* Base implementation of a fluid storage for an empty item.
* The empty item can be filled with an exact amount of some fluid to yield a full item instead.
* The default behavior is to copy the NBT from the empty item to the full item,
* however there is a second constructor that allows customizing the mapping.
*
* <p>For example, an empty bucket could be registered to accept exactly 81000 droplets of water and turn into a water bucket, like that:
* <pre>{@code
* FluidStorage.combinedItemApiProvider(Items.BUCKET) // Go through the combined API provider to make sure other mods can provide storages for empty buckets.
* .register(context -> {// Register a provider for the bucket, returning a new storage every time:
* return new EmptyItemFluidStorage(
* context, // Pass the context.
* Items.WATER_BUCKET, // The result after fluid is inserted.
* Fluids.WATER, // Which fluid to accept.
* FluidConstants.BUCKET // How much fluid to accept.
* );
* });
* }</pre>
* (This is just for illustration purposes! In practice, Fabric API already registers storages for most buckets,
* and it is inefficient to have one storage registered per fluid
* so Fabric API has a storage that accepts any fluid with a corresponding full bucket).
*
* @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 EmptyItemFluidStorage implements InsertionOnlyStorage<FluidVariant> {
private final ContainerItemContext context;
private final Item emptyItem;
private final Function<ItemVariant, ItemVariant> emptyToFullMapping;
private final Fluid insertableFluid;
private final long insertableAmount;
/**
* Create a new instance.
*
* @param context The current context.
* @param fullItem The new item after a successful fill operation.
* @param insertableFluid The fluid that can be inserted. Fluid variant NBT is ignored.
* @param insertableAmount The amount of fluid that can be inserted.
*/
public EmptyItemFluidStorage(ContainerItemContext context, Item fullItem, Fluid insertableFluid, long insertableAmount) {
this(context, emptyVariant -> ItemVariant.of(fullItem, emptyVariant.getNbt()), insertableFluid, insertableAmount);
}
/**
* Create a new instance, with a custom mapping function.
* The mapping function allows customizing how the NBT of the full item depends on the NBT of the empty item.
* The default behavior with the other constructor is to just copy the full NBT.
*
* @param context The current context.
* @param emptyToFullMapping A function mapping the empty item variant, to the variant that should be used for the full item.
* @param insertableFluid The fluid that can be inserted. Fluid variant NBT is ignored on insertion.
* @param insertableAmount The amount of fluid that can be inserted.
* @see #EmptyItemFluidStorage(ContainerItemContext, Item, Fluid, long)
*/
public EmptyItemFluidStorage(ContainerItemContext context, Function<ItemVariant, ItemVariant> emptyToFullMapping, Fluid insertableFluid, long insertableAmount) {
StoragePreconditions.notNegative(insertableAmount);
this.context = context;
this.emptyItem = context.getItemVariant().getItem();
this.emptyToFullMapping = emptyToFullMapping;
this.insertableFluid = insertableFluid;
this.insertableAmount = insertableAmount;
}
@Override
public long insert(FluidVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
// Can't insert if the item is not emptyItem anymore.
if (!context.getItemVariant().isOf(emptyItem)) return 0;
// Make sure that the fluid and amount match.
if (resource.isOf(insertableFluid) && maxAmount >= insertableAmount) {
// If that's ok, just convert one of the empty item into the full item, with the mapping function.
ItemVariant newVariant = emptyToFullMapping.apply(context.getItemVariant());
if (context.exchange(newVariant, 1, transaction) == 1) {
// Conversion ok!
return insertableAmount;
}
}
return 0;
}
@Override
public Iterator<StorageView<FluidVariant>> iterator(TransactionContext transaction) {
return SingleViewIterator.create(new BlankVariantView<>(FluidVariant.blank(), insertableAmount), transaction);
}
}

View file

@ -0,0 +1,136 @@
/*
* 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 java.util.function.Function;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.item.Item;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.base.ExtractionOnlyStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* Base implementation of a fluid storage for a full item.
* The full item contains some fixed amount of a fluid variant, which can be extracted entirely to yield an empty item.
* The default behavior is to copy the NBT from the full item to the empty item,
* however there is a second constructor that allows customizing the mapping.
*
* <p>This is used similarly to {@link EmptyItemFluidStorage}.
*
* @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 FullItemFluidStorage implements ExtractionOnlyStorage<FluidVariant>, SingleSlotStorage<FluidVariant> {
private final ContainerItemContext context;
private final Item fullItem;
private final Function<ItemVariant, ItemVariant> fullToEmptyMapping;
private final FluidVariant containedFluid;
private final long containedAmount;
/**
* Create a new instance.
*
* @param context The current context.
* @param emptyItem The new item after a successful extract operation.
* @param containedFluid The contained fluid variant.
* @param containedAmount How much of {@code containedFluid} is contained.
*/
public FullItemFluidStorage(ContainerItemContext context, Item emptyItem, FluidVariant containedFluid, long containedAmount) {
this(context, fullVariant -> ItemVariant.of(emptyItem, fullVariant.getNbt()), containedFluid, containedAmount);
}
/**
* Create a new instance, with a custom mapping function.
* The mapping function allows customizing how the NBT of the empty item depends on the NBT of the full item.
* The default behavior with the other constructor is to just copy the full NBT.
*
* @param context The current context.
* @param fullToEmptyMapping A function mapping the full item variant, to the variant that should be used
* for the empty item after a successful extract operation.
* @param containedFluid The contained fluid variant.
* @param containedAmount How much of {@code containedFluid} is contained.
*/
public FullItemFluidStorage(ContainerItemContext context, Function<ItemVariant, ItemVariant> fullToEmptyMapping, FluidVariant containedFluid, long containedAmount) {
StoragePreconditions.notBlankNotNegative(containedFluid, containedAmount);
this.context = context;
this.fullItem = context.getItemVariant().getItem();
this.fullToEmptyMapping = fullToEmptyMapping;
this.containedFluid = containedFluid;
this.containedAmount = containedAmount;
}
@Override
public long extract(FluidVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
// If the context's item is not fullItem anymore, can't extract!
if (!context.getItemVariant().isOf(fullItem)) return 0;
// Make sure that the fluid and the amount match.
if (resource.equals(containedFluid) && maxAmount >= containedAmount) {
// If that's ok, just convert one of the full item into the empty item, copying the nbt.
ItemVariant newVariant = fullToEmptyMapping.apply(context.getItemVariant());
if (context.exchange(newVariant, 1, transaction) == 1) {
// Conversion ok!
return containedAmount;
}
}
return 0;
}
@Override
public boolean isResourceBlank() {
return getResource().isBlank();
}
@Override
public FluidVariant getResource() {
// Only contains a resource if the item of the context is still this one.
if (context.getItemVariant().isOf(fullItem)) {
return containedFluid;
} else {
return FluidVariant.blank();
}
}
@Override
public long getAmount() {
if (context.getItemVariant().isOf(fullItem)) {
return containedAmount;
} else {
return 0;
}
}
@Override
public long getCapacity() {
// Capacity is the same as the amount.
return getAmount();
}
}

View file

@ -22,22 +22,15 @@ 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.storage.base.SingleVariantStorage;
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.
* @deprecated Superseded by {@link SingleVariantStorage}. Will be removed in a future iteration of the API.
*/
@ApiStatus.Experimental
@Deprecated
@Deprecated(forRemoval = true)
public abstract class SingleFluidStorage extends SnapshotParticipant<ResourceAmount<FluidVariant>> implements SingleSlotStorage<FluidVariant> {
public FluidVariant fluidVariant = FluidVariant.blank();
public long amount;

View file

@ -0,0 +1,74 @@
/*
* 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.item;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventory;
import net.minecraft.inventory.SidedInventory;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.impl.transfer.item.InventoryStorageImpl;
/**
* An implementation of {@code Storage<ItemVariant>} for vanilla's {@link Inventory}, {@link SidedInventory} and {@link PlayerInventory}.
*
* <p>{@code Inventory} is often nicer to implement than {@code Storage<ItemVariant>}, but harder to use for item transfer.
* This wrapper allows one to have the best of both worlds, for example by storing a subclass of {@link SimpleInventory} in a block entity class,
* while exposing it as a {@code Storage<ItemVariant>} to {@linkplain ItemStorage#SIDED the item transfer API}.
*
* <p>In particular, note that {@link #getSlots} can be combined with {@link CombinedStorage} to retrieve a wrapper around a specific range of slots.
*
* <p><b>Important note:</b> This wrapper assumes that the inventory owns its slots.
* If the inventory does not own its slots, for example because it delegates to another inventory, this wrapper should not be used!
*
* @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 InventoryStorage extends Storage<ItemVariant> {
/**
* Return a wrapper around an {@link Inventory}.
*
* <p>If the inventory is a {@link SidedInventory} and the direction is nonnull, the wrapper wraps the sided inventory from the given direction.
* The returned wrapper contains only the slots with the indices returned by {@link SidedInventory#getAvailableSlots} at query time.
*
* @param inventory The inventory to wrap.
* @param direction The direction to use if the access is sided, or {@code null} if the access is not sided.
*/
static InventoryStorage of(Inventory inventory, @Nullable Direction direction) {
Objects.requireNonNull(inventory, "Null inventory is not supported.");
return InventoryStorageImpl.of(inventory, direction);
}
/**
* Retrieve an unmodifiable list of the wrappers for the slots in this inventory.
* Each wrapper corresponds to a single slot in the inventory.
*/
List<SingleSlotStorage<ItemVariant>> getSlots();
}

View file

@ -0,0 +1,106 @@
/*
* 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.item;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.block.Blocks;
import net.minecraft.block.ChestBlock;
import net.minecraft.block.entity.ChestBlockEntity;
import net.minecraft.inventory.Inventory;
import net.minecraft.inventory.SidedInventory;
import net.minecraft.inventory.SimpleInventory;
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.item.base.SingleStackStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.impl.transfer.item.ComposterWrapper;
import net.fabricmc.fabric.mixin.transfer.DoubleInventoryAccessor;
/**
* Access to {@link Storage Storage&lt;ItemVariant&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 ItemStorage {
/**
* Sided block access to item variant storages.
* 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>Block entities directly implementing {@link Inventory} or {@link SidedInventory} are automatically handled by a fallback provider,
* and don't need to do anything.
* The fallback provider assumes that the {@link Inventory} "owns" its contents. If that's not the case,
* for example because it redirects all function calls to another inventory, then implementing {@link Inventory} should be avoided.
*
* <p>Hoppers and droppers will interact with storages exposed through this lookup, thus implementing one of the vanilla APIs is not necessary.
*
* <p>Depending on the use case, the following strategies can be used to offer a {@code Storage<ItemVariant>} implementation:
* <ul>
* <li>Directly implementing {@code Inventory} or {@code SidedInventory} on a block entity - it will be wrapped automatically.</li>
* <li>Storing an inventory inside a block entity field, and converting it manually with {@link InventoryStorage#of}.
* {@link SimpleInventory} can be used for easy implementation.</li>
* <li>{@link SingleStackStorage} can also be used for more flexibility. Multiple of them can be combined with {@link CombinedStorage}.</li>
* <li>Directly providing a custom implementation of {@code Storage<ItemVariant>} is also possible.</li>
* </ul>
*/
public static final BlockApiLookup<Storage<ItemVariant>, Direction> SIDED =
BlockApiLookup.get(new Identifier("fabric:sided_item_storage"), Storage.asClass(), Direction.class);
private ItemStorage() {
}
static {
// Composter support.
ItemStorage.SIDED.registerForBlocks((world, pos, state, blockEntity, direction) -> ComposterWrapper.get(world, pos, direction), Blocks.COMPOSTER);
// Register Inventory fallback.
ItemStorage.SIDED.registerFallback((world, pos, state, blockEntity, direction) -> {
Inventory inventoryToWrap = null;
if (blockEntity instanceof Inventory inventory) {
if (blockEntity instanceof ChestBlockEntity && state.getBlock() instanceof ChestBlock chestBlock) {
inventoryToWrap = ChestBlock.getInventory(chestBlock, state, world, pos, true);
// For double chests, we need to retrieve a wrapper for each part separately.
if (inventoryToWrap instanceof DoubleInventoryAccessor accessor) {
Storage<ItemVariant> first = InventoryStorage.of(accessor.fabric_getFirst(), direction);
Storage<ItemVariant> second = InventoryStorage.of(accessor.fabric_getSecond(), direction);
return new CombinedStorage<>(List.of(first, second));
}
} else {
inventoryToWrap = inventory;
}
}
return inventoryToWrap != null ? InventoryStorage.of(inventoryToWrap, direction) : null;
});
}
}

View file

@ -0,0 +1,121 @@
/*
* 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.item;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.Item;
import net.minecraft.item.ItemConvertible;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.fabricmc.fabric.impl.transfer.item.ItemVariantImpl;
import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant;
/**
* An immutable count-less ItemStack, i.e. an immutable association of an item and an optional NBT compound tag.
*
* <p>Do not implement, use the static {@code of(...)} functions instead.
*
* @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 ItemVariant extends TransferVariant<Item> {
/**
* Retrieve a blank ItemVariant.
*/
static ItemVariant blank() {
return of(Items.AIR);
}
/**
* Retrieve an ItemVariant with the item and tag of a stack.
*/
static ItemVariant of(ItemStack stack) {
return of(stack.getItem(), stack.getTag());
}
/**
* Retrieve an ItemVariant with an item and without a tag.
*/
static ItemVariant of(ItemConvertible item) {
return of(item, null);
}
/**
* Retrieve an ItemVariant with an item and an optional tag.
*/
static ItemVariant of(ItemConvertible item, @Nullable NbtCompound tag) {
return ItemVariantImpl.of(item.asItem(), tag);
}
/**
* Return true if the item and tag of this variant match those of the passed stack, and false otherwise.
*/
default boolean matches(ItemStack stack) {
return isOf(stack.getItem()) && nbtMatches(stack.getTag());
}
/**
* Return the item of this variant.
*/
default Item getItem() {
return getObject();
}
/**
* Create a new item stack with count 1 from this variant.
*/
default ItemStack toStack() {
return toStack(1);
}
/**
* Create a new item stack from this variant.
*
* @param count The count of the returned stack. It may lead to counts higher than maximum stack size.
*/
default ItemStack toStack(int count) {
if (isBlank()) return ItemStack.EMPTY;
ItemStack stack = new ItemStack(getItem(), count);
stack.setTag(copyNbt());
return stack;
}
/**
* Deserialize a variant from an NBT compound tag, assuming it was serialized using
* {@link #toNbt}. If an error occurs during deserialization, it will be logged
* with the DEBUG level, and a blank variant will be returned.
*/
static ItemVariant fromNbt(NbtCompound nbt) {
return ItemVariantImpl.fromNbt(nbt);
}
/**
* Write a variant from a packet byte buffer, assuming it was serialized using
* {@link #toPacket}.
*/
static ItemVariant fromPacket(PacketByteBuf buf) {
return ItemVariantImpl.fromPacket(buf);
}
}

View file

@ -0,0 +1,79 @@
/*
* 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.item;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.ScreenHandler;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import net.fabricmc.fabric.impl.transfer.item.CursorSlotWrapper;
/**
* A {@code Storage<ItemVariant>} implementation for a {@link PlayerInventory}.
* This is a specialized version of {@link InventoryStorage},
* with an additional transactional wrapper for {@link PlayerInventory#offerOrDrop}.
*
* <p>Note that this is a wrapper around all the slots of the player inventory.
* This may cause direct insertion to insert arbitrary items into equipment slots or other unexpected behavior.
* To prevent this, {@link #offerOrDrop} is recommended for simple insertions.
* {@link #getSlots} can also be used and combined with {@link CombinedStorage} to retrieve a wrapper around a specific range of slots.
*
* @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 PlayerInventoryStorage extends InventoryStorage {
/**
* Return an instance for the passed player's inventory.
*/
static PlayerInventoryStorage of(PlayerEntity player) {
return of(player.getInventory());
}
/**
* Return an instance for the passed player inventory.
*/
static PlayerInventoryStorage of(PlayerInventory playerInventory) {
return (PlayerInventoryStorage) InventoryStorage.of(playerInventory, null);
}
/**
* Return a wrapper around the cursor slot of a screen handler,
* i.e. the stack that can be manipulated with {@link ScreenHandler#getCursorStack()} and {@link ScreenHandler#setCursorStack}.
*/
static SingleSlotStorage<ItemVariant> getCursorStorage(ScreenHandler screenHandler) {
return CursorSlotWrapper.get(screenHandler);
}
/**
* Add items to the inventory if possible, and drop any leftover items in the world, similar to {@link PlayerInventory#offerOrDrop}
*
* <p>Note: This function has full transaction support, and will not actually drop the items until the outermost transaction is committed.
*
* @param variant The variant to insert.
* @param amount How many of the variant to insert.
* @param transaction The transaction this operation is part of.
*/
void offerOrDrop(ItemVariant variant, long amount, TransactionContext transaction);
}

View file

@ -0,0 +1,164 @@
/*
* 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.item.base;
import org.jetbrains.annotations.ApiStatus;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
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;
/**
* An item variant storage backed by an {@link ItemStack}.
* Implementors should at least override {@link #getStack} and {@link #setStack},
* and probably {@link #onFinalCommit} as well for {@code markDirty()} and similar calls.
*
* <p>{@link #canInsert} and {@link #canExtract} can be used for more precise control over which items 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}.
* {@link #getCapacity(ItemVariant)} can be overridden to change the maximum capacity depending on the item variant.
*
* @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 SingleStackStorage extends SnapshotParticipant<ItemStack> implements SingleSlotStorage<ItemVariant> {
/**
* Return the stack of this storage. It will be modified directly sometimes to avoid needless copies.
* However, any mutation of the stack will directly be followed by a call to {@link #setStack}.
* This means that either returning the backing stack directly or a copy is safe.
*
* @return The current stack.
*/
protected abstract ItemStack getStack();
/**
* Set the stack of this storage.
*/
protected abstract void setStack(ItemStack stack);
/**
* Return {@code true} if the passed non-blank item variant can be inserted, {@code false} otherwise.
*/
protected boolean canInsert(ItemVariant itemVariant) {
return true;
}
/**
* Return {@code true} if the passed non-blank item variant can be extracted, {@code false} otherwise.
*/
protected boolean canExtract(ItemVariant itemVariant) {
return true;
}
/**
* Return the maximum capacity of this storage for the passed item variant.
* If the passed item variant is blank, an estimate should be returned.
*
* <p>If the capacity should be limited by the max count of the item, this function must take it into account.
* For example, a storage with a maximum count of 4, or less for items that have a smaller max count,
* should override this to return {@code Math.min(itemVariant.getItem().getMaxCount(), 4);}.
*
* @return The maximum capacity of this storage for the passed item variant.
*/
protected int getCapacity(ItemVariant itemVariant) {
return itemVariant.getItem().getMaxCount();
}
@Override
public final boolean isResourceBlank() {
return getResource().isBlank();
}
@Override
public final ItemVariant getResource() {
return ItemVariant.of(getStack());
}
@Override
public final long getAmount() {
return getStack().getCount();
}
@Override
public final long getCapacity() {
return getCapacity(getResource());
}
@Override
public final long insert(ItemVariant insertedVariant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(insertedVariant, maxAmount);
ItemStack currentStack = getStack();
if ((insertedVariant.matches(currentStack) || currentStack.isEmpty()) && canInsert(insertedVariant)) {
int insertedAmount = (int) Math.min(maxAmount, getCapacity(insertedVariant) - currentStack.getCount());
if (insertedAmount > 0) {
updateSnapshots(transaction);
if (currentStack.isEmpty()) {
currentStack = insertedVariant.toStack(insertedAmount);
} else {
currentStack.increment(insertedAmount);
}
setStack(currentStack);
}
return insertedAmount;
}
return 0;
}
@Override
public final long extract(ItemVariant variant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(variant, maxAmount);
ItemStack currentStack = getStack();
if (variant.matches(currentStack) && canExtract(variant)) {
int extracted = (int) Math.min(currentStack.getCount(), maxAmount);
if (extracted > 0) {
this.updateSnapshots(transaction);
currentStack.decrement(extracted);
setStack(currentStack);
}
return extracted;
}
return 0;
}
@Override
protected final ItemStack createSnapshot() {
return getStack().copy();
}
@Override
protected final void readSnapshot(ItemStack snapshot) {
setStack(snapshot);
}
}

View file

@ -39,16 +39,16 @@
* wrong usage of {@code Storage} and {@code StorageView} methods.
* </p>
*
* <p>Implementors of transfer variant storages with a fixed number of "slots" or "tanks" can use
* {@link net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage SingleVariantStorage},
* and combine them with {@link net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage CombinedStorage}.
*
* <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}.
* Instances can be accessed through the API lookups 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.
*
@ -56,5 +56,21 @@
* 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}.
*
* <p><h2>Item transfer</h2>
* A {@code Storage<ItemVariant>} is any object that can store items.
* Instances can be accessed through the API lookup defined in {@link net.fabricmc.fabric.api.transfer.v1.item.ItemStorage ItemStorage}.
* </p>
*
* <p>The lookup already provides compatibility with vanilla inventories, however it may sometimes be interesting to use
* {@link net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage InventoryStorage} or
* {@link net.fabricmc.fabric.api.transfer.v1.item.PlayerInventoryStorage PlayerInventoryStorage} when interaction with
* {@code Inventory}-based APIs is required.
*
* <p><h2>{@code ContainerItemContext}</h2>
* {@link net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext ContainerItemContext} is a context designed for {@code ItemApiLookup} queries
* that allows the returned APIs to interact with the containing inventory.
* Notably, it is used by the {@code FluidStorage.ITEM} lookup for fluid-containing items.
* </p>
*/
package net.fabricmc.fabric.api.transfer.v1;

View file

@ -119,6 +119,7 @@ public interface Storage<T> {
/**
* 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.
* The iterator doesn't guarantee that a single resource only occurs once during an iteration.
*
* <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.

View file

@ -21,6 +21,11 @@ import java.util.function.Predicate;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.minecraft.inventory.Inventory;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.util.math.MathHelper;
import net.fabricmc.fabric.api.transfer.v1.storage.base.ResourceAmount;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
@ -73,7 +78,7 @@ public final class StorageUtil {
long totalMoved = 0;
try (Transaction iterationTransaction = (transaction == null ? Transaction.openOuter() : transaction.openNested())) {
try (Transaction iterationTransaction = Transaction.openNested(transaction)) {
for (StorageView<T> view : from.iterable(iterationTransaction)) {
if (view.isResourceBlank()) continue;
T resource = view.getResource();
@ -154,7 +159,7 @@ public final class StorageUtil {
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()) {
try (Transaction nested = Transaction.openNested(transaction)) {
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();
@ -168,4 +173,67 @@ public final class StorageUtil {
return null;
}
/**
* Attempt to find a resource stored in the passed storage that can be extracted, and how much of it 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 and the strictly positive amount of it that can be extracted,
* or {@code null} if none could be found.
*/
@Nullable
public static <T> ResourceAmount<T> findExtractableContent(@Nullable Storage<T> storage, @Nullable TransactionContext transaction) {
T extractableResource = findExtractableResource(storage, transaction);
if (extractableResource != null) {
try (Transaction nested = Transaction.openNested(transaction)) {
long extractableAmount = storage.extract(extractableResource, Long.MAX_VALUE, nested);
if (extractableAmount > 0) {
return new ResourceAmount<>(extractableResource, extractableAmount);
}
}
}
return null;
}
/**
* Compute the comparator output for a storage, similar to {@link ScreenHandler#calculateComparatorOutput(Inventory)}.
*
* @param storage The storage for which the comparator level should be computed.
* @param transaction The current transaction, or {@code null} if a transaction should be opened for this computation.
* @param <T> The type of the stored resources.
* @return An integer between 0 and 15 (inclusive): the comparator output for the passed storage.
*/
public static <T> int calculateComparatorOutput(@Nullable Storage<T> storage, @Nullable TransactionContext transaction) {
if (storage == null) return 0;
if (transaction == null) {
try (Transaction outer = Transaction.openOuter()) {
return calculateComparatorOutputInner(storage, outer);
}
} else {
return calculateComparatorOutputInner(storage, transaction);
}
}
private static <T> int calculateComparatorOutputInner(Storage<T> storage, TransactionContext transaction) {
double fillPercentage = 0;
int viewCount = 0;
boolean hasNonEmptyView = false;
for (StorageView<T> view : storage.iterable(transaction)) {
viewCount++;
if (view.getAmount() > 0) {
fillPercentage += (double) view.getAmount() / view.getCapacity();
hasNonEmptyView = true;
}
}
return MathHelper.floor(fillPercentage / viewCount * 14) + (hasNonEmptyView ? 1 : 0);
}
}

View file

@ -0,0 +1,75 @@
/*
* 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.StorageView;
import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A transfer variant storage view that contains a blank variant all the time (it's always empty), but may have a nonzero capacity.
* This can be used to give capacity hints even if the storage is empty.
*
* @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 BlankVariantView<T extends TransferVariant<?>> implements StorageView<T> {
private final T blankVariant;
private final long capacity;
/**
* Create a new instance.
* @throws IllegalArgumentException If the passed {@code blankVariant} is not blank.
*/
public BlankVariantView(T blankVariant, long capacity) {
if (!blankVariant.isBlank()) {
throw new IllegalArgumentException("Expected a blank variant, received " + blankVariant);
}
this.blankVariant = blankVariant;
this.capacity = capacity;
}
@Override
public long extract(T resource, long maxAmount, TransactionContext transaction) {
return 0; // can't extract
}
@Override
public boolean isResourceBlank() {
return true;
}
@Override
public T getResource() {
return blankVariant;
}
@Override
public long getAmount() {
return 0;
}
@Override
public long getCapacity() {
return capacity;
}
}

View file

@ -42,7 +42,7 @@ import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
@ApiStatus.Experimental
@Deprecated
public class CombinedStorage<T, S extends Storage<T>> implements Storage<T> {
public final List<S> parts;
public List<S> parts;
public CombinedStorage(List<S> parts) {
this.parts = parts;

View file

@ -0,0 +1,145 @@
/*
* 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.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.TransferVariant;
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 transfer variant at any given time.
* Implementors should at least override {@link #getCapacity(TransferVariant)},
* and probably {@link #onFinalCommit} as well for {@code markDirty()} and similar calls.
*
* <p>{@link #canInsert} and {@link #canExtract} can be used for more precise control over which variants 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 SingleVariantStorage<T extends TransferVariant<?>> extends SnapshotParticipant<ResourceAmount<T>> implements SingleSlotStorage<T> {
public T variant = getBlankVariant();
public long amount = 0;
/**
* Return the blank variant.
*/
protected abstract T getBlankVariant();
/**
* Return the maximum capacity of this storage for the passed transfer variant.
* If the passed variant is blank, an estimate should be returned.
*/
protected abstract long getCapacity(T variant);
/**
* @return {@code true} if the passed non-blank variant can be inserted, {@code false} otherwise.
*/
protected boolean canInsert(T variant) {
return true;
}
/**
* @return {@code true} if the passed non-blank variant can be extracted, {@code false} otherwise.
*/
protected boolean canExtract(T variant) {
return true;
}
@Override
public long insert(T insertedVariant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(insertedVariant, maxAmount);
if ((insertedVariant.equals(variant) || variant.isBlank()) && canInsert(insertedVariant)) {
long insertedAmount = Math.min(maxAmount, getCapacity(insertedVariant) - amount);
if (insertedAmount > 0) {
updateSnapshots(transaction);
if (variant.isBlank()) {
variant = insertedVariant;
amount = insertedAmount;
} else {
amount += insertedAmount;
}
}
return insertedAmount;
}
return 0;
}
@Override
public long extract(T extractedVariant, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(extractedVariant, maxAmount);
if (extractedVariant.equals(variant) && canExtract(extractedVariant)) {
long extractedAmount = Math.min(maxAmount, amount);
if (extractedAmount > 0) {
updateSnapshots(transaction);
amount -= extractedAmount;
if (amount == 0) {
variant = getBlankVariant();
}
}
return extractedAmount;
}
return 0;
}
@Override
public boolean isResourceBlank() {
return variant.isBlank();
}
@Override
public T getResource() {
return variant;
}
@Override
public long getAmount() {
return amount;
}
@Override
public long getCapacity() {
return getCapacity(variant);
}
@Override
protected ResourceAmount<T> createSnapshot() {
return new ResourceAmount<>(variant, amount);
}
@Override
protected void readSnapshot(ResourceAmount<T> snapshot) {
variant = snapshot.resource();
amount = snapshot.amount();
}
}

View file

@ -17,6 +17,7 @@
package net.fabricmc.fabric.api.transfer.v1.transaction;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
import net.fabricmc.fabric.impl.transfer.transaction.TransactionManagerImpl;
@ -96,6 +97,13 @@ public interface Transaction extends AutoCloseable, TransactionContext {
return TransactionManagerImpl.MANAGERS.get().isOpen();
}
/**
* Open a nested transaction if {@code maybeParent} is non null, or an outer transaction if {@code maybeParent} is null.
*/
static Transaction openNested(@Nullable TransactionContext maybeParent) {
return maybeParent == null ? openOuter() : maybeParent.openNested();
}
/**
* Close the current transaction, rolling back all the changes that happened during this transaction and
* the transactions opened with {@link #openNested} from this transaction.

View file

@ -79,7 +79,7 @@ public abstract class SnapshotParticipant<T> implements Transaction.CloseCallbac
* 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) {
public void updateSnapshots(TransactionContext transaction) {
// Make sure we have enough storage for snapshots
while (snapshots.size() <= transaction.nestingDepth()) {
snapshots.add(null);
@ -96,7 +96,7 @@ public abstract class SnapshotParticipant<T> implements Transaction.CloseCallbac
}
@Override
public final void onClose(TransactionContext transaction, Transaction.Result result) {
public void onClose(TransactionContext transaction, Transaction.Result result) {
// Get and remove the relevant snapshot.
T snapshot = snapshots.set(transaction.nestingDepth(), null);
@ -121,7 +121,7 @@ public abstract class SnapshotParticipant<T> implements Transaction.CloseCallbac
}
@Override
public final void afterOuterClose(Transaction.Result result) {
public 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,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.impl.transfer.context;
import java.util.Collections;
import java.util.List;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
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.storage.base.SingleVariantStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
public class InitialContentsContainerItemContext implements ContainerItemContext {
private final SingleVariantStorage<ItemVariant> backingSlot = new SingleVariantStorage<>() {
@Override
protected ItemVariant getBlankVariant() {
return ItemVariant.blank();
}
@Override
protected long getCapacity(ItemVariant variant) {
return Long.MAX_VALUE;
}
};
public InitialContentsContainerItemContext(ItemVariant initialVariant, long initialAmount) {
backingSlot.variant = initialVariant;
backingSlot.amount = initialAmount;
}
@Override
public SingleSlotStorage<ItemVariant> getMainSlot() {
return backingSlot;
}
@Override
public long insertOverflow(ItemVariant itemVariant, long maxAmount, TransactionContext transactionContext) {
StoragePreconditions.notBlankNotNegative(itemVariant, maxAmount);
// Always allow anything to be inserted.
return maxAmount;
}
@Override
public List<SingleSlotStorage<ItemVariant>> getAdditionalSlots() {
return Collections.emptyList();
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.context;
import java.util.List;
import java.util.Objects;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.util.Hand;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.item.PlayerInventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
public class PlayerContainerItemContext implements ContainerItemContext {
private final PlayerInventoryStorage playerWrapper;
private final SingleSlotStorage<ItemVariant> slot;
public PlayerContainerItemContext(PlayerEntity player, Hand hand) {
Objects.requireNonNull(hand, "Hand may not be null.");
this.playerWrapper = PlayerInventoryStorage.of(player);
int slotIndex = hand == Hand.MAIN_HAND ? player.getInventory().selectedSlot : PlayerInventory.OFF_HAND_SLOT;
this.slot = playerWrapper.getSlots().get(slotIndex);
}
public PlayerContainerItemContext(PlayerEntity player, SingleSlotStorage<ItemVariant> slot) {
this.playerWrapper = PlayerInventoryStorage.of(player);
this.slot = slot;
}
@Override
public SingleSlotStorage<ItemVariant> getMainSlot() {
return slot;
}
@Override
public long insertOverflow(ItemVariant itemVariant, long maxAmount, TransactionContext transactionContext) {
playerWrapper.offerOrDrop(itemVariant, maxAmount, transactionContext);
return maxAmount;
}
@Override
public List<SingleSlotStorage<ItemVariant>> getAdditionalSlots() {
return playerWrapper.getSlots();
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.context;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
public class SingleSlotContainerItemContext implements ContainerItemContext {
private final SingleSlotStorage<ItemVariant> slot;
public SingleSlotContainerItemContext(SingleSlotStorage<ItemVariant> slot) {
this.slot = Objects.requireNonNull(slot);
}
@Override
public SingleSlotStorage<ItemVariant> getMainSlot() {
return slot;
}
@Override
public long insertOverflow(ItemVariant itemVariant, long maxAmount, TransactionContext transactionContext) {
return 0;
}
@Override
public List<SingleSlotStorage<ItemVariant>> getAdditionalSlots() {
return Collections.emptyList();
}
}

View file

@ -54,8 +54,7 @@ public class CauldronStorage extends SnapshotParticipant<BlockState> implements
public static CauldronStorage get(World world, BlockPos pos) {
WorldLocation location = new WorldLocation(world, pos.toImmutable());
CAULDRONS.computeIfAbsent(location, CauldronStorage::new);
return CAULDRONS.get(location);
return CAULDRONS.computeIfAbsent(location, CauldronStorage::new);
}
private final WorldLocation location;

View file

@ -0,0 +1,100 @@
/*
* 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.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.fabricmc.fabric.api.lookup.v1.item.ItemApiLookup;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
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.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
public class CombinedProvidersImpl {
public static Event<FluidStorage.CombinedItemApiProvider> createEvent(boolean invokeFallback) {
return EventFactory.createArrayBacked(FluidStorage.CombinedItemApiProvider.class, listeners -> context -> {
List<Storage<FluidVariant>> storages = new ArrayList<>();
for (FluidStorage.CombinedItemApiProvider listener : listeners) {
Storage<FluidVariant> found = listener.find(context);
if (found != null) {
storages.add(found);
}
}
// Allow combining per-item combined providers with fallback combined providers.
if (!storages.isEmpty() && invokeFallback) {
// Only invoke the fallback if API Lookup doesn't invoke it right after,
// that is only invoke the fallback if storages were offered,
// otherwise we can wait for API Lookup to invoke the fallback provider itself.
Storage<FluidVariant> fallbackFound = FluidStorage.GENERAL_COMBINED_PROVIDER.invoker().find(context);
if (fallbackFound != null) {
storages.add(fallbackFound);
}
}
return storages.isEmpty() ? null : new CombinedStorage<>(storages);
});
}
private static class Provider implements ItemApiLookup.ItemApiProvider<Storage<FluidVariant>, ContainerItemContext> {
private final Event<FluidStorage.CombinedItemApiProvider> event = createEvent(true);
@Override
@Nullable
public Storage<FluidVariant> find(ItemStack itemStack, ContainerItemContext context) {
if (!context.getItemVariant().matches(itemStack)) {
String errorMessage = String.format(
"Query stack %s and ContainerItemContext variant %s don't match.",
itemStack,
context.getItemVariant()
);
throw new IllegalArgumentException(errorMessage);
}
return event.invoker().find(context);
}
}
public static Event<FluidStorage.CombinedItemApiProvider> getOrCreateItemEvent(Item item) {
// register here is thread-safe, so the query below will return a valid provider (possibly one registered before or from another thread).
FluidStorage.ITEM.registerForItems(new Provider(), item);
ItemApiLookup.ItemApiProvider<Storage<FluidVariant>, ContainerItemContext> existingProvider = FluidStorage.ITEM.getProvider(item);
if (existingProvider instanceof Provider registeredProvider) {
return registeredProvider.event;
} else {
String errorMessage = String.format(
"An incompatible provider was already registered for item %s. Provider: %s.",
item,
existingProvider
);
throw new IllegalStateException(errorMessage);
}
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.Iterator;
import net.minecraft.item.Item;
import net.minecraft.item.Items;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
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.item.ItemVariant;
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.BlankVariantView;
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.TransactionContext;
import net.fabricmc.fabric.mixin.transfer.BucketItemAccessor;
/**
* Storage implementation for empty buckets, accepting any fluid with a bidirectional fluid &lt;-&gt; bucket mapping.
*/
public class EmptyBucketStorage implements InsertionOnlyStorage<FluidVariant> {
private final ContainerItemContext context;
public EmptyBucketStorage(ContainerItemContext context) {
this.context = context;
}
@Override
public long insert(FluidVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
if (!context.getItemVariant().isOf(Items.BUCKET)) return 0;
Item fullBucket = resource.getFluid().getBucketItem();
// Make sure the resource is a correct fluid mapping: the fluid <-> bucket mapping must be bidirectional.
if (fullBucket instanceof BucketItemAccessor accessor && resource.isOf(accessor.fabric_getFluid())) {
if (maxAmount >= FluidConstants.BUCKET) {
ItemVariant newVariant = ItemVariant.of(fullBucket, context.getItemVariant().getNbt());
if (context.exchange(newVariant, 1, transaction) == 1) {
return FluidConstants.BUCKET;
}
}
}
return 0;
}
@Override
public Iterator<StorageView<FluidVariant>> iterator(TransactionContext transaction) {
return SingleViewIterator.create(new BlankVariantView<>(FluidVariant.blank(), FluidConstants.BUCKET), transaction);
}
}

View file

@ -0,0 +1,117 @@
/*
* 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 org.jetbrains.annotations.Nullable;
import net.minecraft.fluid.Fluids;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.potion.PotionUtil;
import net.minecraft.potion.Potions;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
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.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.StoragePreconditions;
import net.fabricmc.fabric.api.transfer.v1.storage.base.ExtractionOnlyStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* Implementation of the storage for a water potion.
*/
public class WaterPotionStorage implements ExtractionOnlyStorage<FluidVariant>, SingleSlotStorage<FluidVariant> {
private static final FluidVariant CONTAINED_FLUID = FluidVariant.of(Fluids.WATER);
private static final long CONTAINED_AMOUNT = FluidConstants.BOTTLE;
@Nullable
public static WaterPotionStorage find(ContainerItemContext context) {
return isWaterPotion(context) ? new WaterPotionStorage(context) : null;
}
private static boolean isWaterPotion(ContainerItemContext context) {
ItemVariant variant = context.getItemVariant();
return variant.isOf(Items.POTION) && PotionUtil.getPotion(variant.getNbt()) == Potions.WATER;
}
private final ContainerItemContext context;
private WaterPotionStorage(ContainerItemContext context) {
this.context = context;
}
private boolean isWaterPotion() {
return isWaterPotion(context);
}
private ItemVariant mapToGlassBottle() {
ItemStack newStack = context.getItemVariant().toStack();
PotionUtil.setPotion(newStack, Potions.EMPTY);
return ItemVariant.of(Items.GLASS_BOTTLE, newStack.getTag());
}
@Override
public long extract(FluidVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
// Not a water potion anymore
if (!isWaterPotion()) return 0;
// Make sure that the fluid and the amount match.
if (resource.equals(CONTAINED_FLUID) && maxAmount >= CONTAINED_AMOUNT) {
if (context.exchange(mapToGlassBottle(), 1, transaction) == 1) {
// Conversion ok!
return CONTAINED_AMOUNT;
}
}
return 0;
}
@Override
public boolean isResourceBlank() {
return getResource().isBlank();
}
@Override
public FluidVariant getResource() {
// Only contains a resource if this is still a water potion.
if (isWaterPotion()) {
return CONTAINED_FLUID;
} else {
return FluidVariant.blank();
}
}
@Override
public long getAmount() {
if (isWaterPotion()) {
return CONTAINED_AMOUNT;
} else {
return 0;
}
}
@Override
public long getCapacity() {
// Capacity is the same as the amount.
return getAmount();
}
}

View file

@ -0,0 +1,203 @@
/*
* 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.item;
import static net.minecraft.util.math.Direction.UP;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import com.google.common.collect.MapMaker;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.BlockState;
import net.minecraft.block.ComposterBlock;
import net.minecraft.item.Items;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import net.minecraft.world.WorldEvents;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
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.storage.base.ExtractionOnlyStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.InsertionOnlyStorage;
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;
/**
* Implementation of {@code Storage<ItemVariant>} for composters.
*/
public class ComposterWrapper extends SnapshotParticipant<Float> {
// Record is used for convenient constructor, hashcode and equals implementations.
private record WorldLocation(World world, BlockPos pos) {
private BlockState getBlockState() {
return world.getBlockState(pos);
}
private void setBlockState(BlockState state) {
world.setBlockState(pos, state);
}
}
// Weak values to make sure wrappers are cleaned up after use, thread-safe.
// The two storages strongly reference the containing wrapper, so we are alright with weak values.
private static final Map<WorldLocation, ComposterWrapper> COMPOSTERS = new MapMaker().concurrencyLevel(1).weakValues().makeMap();
@Nullable
public static Storage<ItemVariant> get(World world, BlockPos pos, Direction direction) {
Objects.requireNonNull(direction);
if (direction.getAxis().isVertical()) {
WorldLocation location = new WorldLocation(world, pos.toImmutable());
ComposterWrapper composterWrapper = COMPOSTERS.computeIfAbsent(location, ComposterWrapper::new);
return direction == UP ? composterWrapper.upStorage : composterWrapper.downStorage;
} else {
return null;
}
}
private static final float DO_NOTHING = 0f;
private static final float EXTRACT_BONEMEAL = -1f;
private final WorldLocation location;
// -1 if bonemeal was extracted, otherwise the composter increase probability of the (pending) inserted item.
private Float increaseProbability = DO_NOTHING;
private final TopStorage upStorage = new TopStorage();
private final BottomStorage downStorage = new BottomStorage();
private ComposterWrapper(WorldLocation location) {
this.location = location;
}
@Override
protected Float createSnapshot() {
return increaseProbability;
}
@Override
protected void readSnapshot(Float snapshot) {
// Reset after unsuccessful commit.
increaseProbability = snapshot;
}
@Override
protected void onFinalCommit() {
// Apply pending action
if (increaseProbability == EXTRACT_BONEMEAL) {
// Mimic ComposterBlock#emptyComposter logic.
location.setBlockState(location.getBlockState().with(ComposterBlock.LEVEL, 0));
// Play the sound
} else if (increaseProbability > 0) {
boolean increaseSuccessful = location.world.getRandom().nextDouble() < increaseProbability;
if (increaseSuccessful) {
// Mimic ComposterBlock#addToComposter logic.
BlockState state = location.getBlockState();
int newLevel = state.get(ComposterBlock.LEVEL) + 1;
BlockState newState = state.with(ComposterBlock.LEVEL, newLevel);
location.setBlockState(newState);
if (newLevel == 7) {
location.world.getBlockTickScheduler().schedule(location.pos, state.getBlock(), 20);
}
}
location.world.syncWorldEvent(WorldEvents.COMPOSTER_USED, location.pos, increaseSuccessful ? 1 : 0);
}
// Reset after successful commit.
increaseProbability = DO_NOTHING;
}
private class TopStorage implements InsertionOnlyStorage<ItemVariant> {
@Override
public long insert(ItemVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
// Check amount.
if (maxAmount < 1) return 0;
// Check that no action is scheduled.
if (increaseProbability != DO_NOTHING) return 0;
// Check that the composter can accept items.
if (location.getBlockState().get(ComposterBlock.LEVEL) >= 7) return 0;
// Check that the item is compostable.
float insertedIncreaseProbability = ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE.getFloat(resource.getItem());
if (insertedIncreaseProbability <= 0) return 0;
// Schedule insertion.
updateSnapshots(transaction);
increaseProbability = insertedIncreaseProbability;
return 1;
}
@Override
public Iterator<StorageView<ItemVariant>> iterator(TransactionContext transaction) {
return Collections.emptyIterator();
}
}
private class BottomStorage implements ExtractionOnlyStorage<ItemVariant>, SingleSlotStorage<ItemVariant> {
private static final ItemVariant BONE_MEAL = ItemVariant.of(Items.BONE_MEAL);
private boolean hasBoneMeal() {
// We only have bone meal if the level is 8 and no action was scheduled.
return increaseProbability == DO_NOTHING && location.getBlockState().get(ComposterBlock.LEVEL) == 8;
}
@Override
public long extract(ItemVariant resource, long maxAmount, TransactionContext transaction) {
StoragePreconditions.notBlankNotNegative(resource, maxAmount);
// Check amount.
if (maxAmount < 1) return 0;
// Check that the resource is bone meal.
if (!BONE_MEAL.equals(resource)) return 0;
// Check that there is bone meal to extract.
if (!hasBoneMeal()) return 0;
updateSnapshots(transaction);
increaseProbability = EXTRACT_BONEMEAL;
return 1;
}
@Override
public boolean isResourceBlank() {
return getResource().isBlank();
}
@Override
public ItemVariant getResource() {
return BONE_MEAL;
}
@Override
public long getAmount() {
return hasBoneMeal() ? 1 : 0;
}
@Override
public long getCapacity() {
return 1;
}
}
}

View file

@ -0,0 +1,53 @@
/*
* 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.item;
import java.util.Map;
import com.google.common.collect.MapMaker;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.ScreenHandler;
import net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage;
/**
* Wrapper around the cursor slot of a screen handler.
*/
public class CursorSlotWrapper extends SingleStackStorage {
private static final Map<ScreenHandler, CursorSlotWrapper> WRAPPERS = new MapMaker().weakValues().makeMap();
public static CursorSlotWrapper get(ScreenHandler screenHandler) {
return WRAPPERS.computeIfAbsent(screenHandler, CursorSlotWrapper::new);
}
private final ScreenHandler screenHandler;
private CursorSlotWrapper(ScreenHandler screenHandler) {
this.screenHandler = screenHandler;
}
@Override
protected ItemStack getStack() {
return screenHandler.getCursorStack();
}
@Override
protected void setStack(ItemStack stack) {
screenHandler.setCursorStack(stack);
}
}

View file

@ -0,0 +1,69 @@
/*
* 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.item;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.transfer.v1.item.base.SingleStackStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* A wrapper around a single slot of an inventory.
* We must ensure that only one instance of this class exists for every inventory slot,
* or the transaction logic will not work correctly.
* This is handled by the Map in InventoryStorageImpl.
*/
class InventorySlotWrapper extends SingleStackStorage {
/**
* The strong reference to the InventoryStorageImpl ensures that the weak value doesn't get GC'ed when individual slots are still being accessed.
*/
private final InventoryStorageImpl storage;
final int slot;
InventorySlotWrapper(InventoryStorageImpl storage, int slot) {
this.storage = storage;
this.slot = slot;
}
@Override
protected ItemStack getStack() {
return storage.inventory.getStack(slot);
}
@Override
protected void setStack(ItemStack stack) {
storage.inventory.setStack(slot, stack);
}
@Override
protected boolean canInsert(ItemVariant itemVariant) {
return storage.inventory.isValid(slot, itemVariant.toStack());
}
@Override
public int getCapacity(ItemVariant variant) {
return Math.min(storage.inventory.getMaxCountPerStack(), variant.getItem().getMaxCount());
}
// We override updateSnapshots to also schedule a markDirty call for the backing inventory.
@Override
public void updateSnapshots(TransactionContext transaction) {
storage.markDirtyParticipant.updateSnapshots(transaction);
super.updateSnapshots(transaction);
}
}

View file

@ -0,0 +1,131 @@
/*
* 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.item;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.MapMaker;
import org.jetbrains.annotations.Nullable;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventory;
import net.minecraft.inventory.SidedInventory;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.base.SnapshotParticipant;
/**
* Implementation of {@link InventoryStorage}.
* Note on thread-safety: we assume that Inventory's are inherently single-threaded, and no attempt is made at synchronization.
* However, the access to implementations can happen on multiple threads concurrently, which is why we use a thread-safe wrapper map.
*/
public class InventoryStorageImpl extends CombinedStorage<ItemVariant, SingleSlotStorage<ItemVariant>> implements InventoryStorage {
/**
* Global wrapper concurrent map.
*
* <p>A note on GC: weak keys alone are not suitable as the InventoryStorage slots strongly reference the Inventory keys.
* Weak values are suitable, but we have to ensure that the InventoryStorageImpl remains strongly reachable as long as
* one of the slot wrappers refers to it, hence the {@code strongRef} field in {@link InventorySlotWrapper}.
*/
// TODO: look into promoting the weak reference to a soft reference if building the wrappers becomes a performance bottleneck.
// TODO: should have identity semantics?
private static final Map<Inventory, InventoryStorageImpl> WRAPPERS = new MapMaker().weakValues().makeMap();
public static InventoryStorage of(Inventory inventory, @Nullable Direction direction) {
InventoryStorageImpl storage = WRAPPERS.computeIfAbsent(inventory, inv -> {
if (inv instanceof PlayerInventory playerInventory) {
return new PlayerInventoryStorageImpl(playerInventory);
} else {
return new InventoryStorageImpl(inv);
}
});
storage.resizeSlotList();
return storage.getSidedWrapper(direction);
}
final Inventory inventory;
/**
* This {@code backingList} is the real list of wrappers.
* The {@code parts} in the superclass is the public-facing unmodifiable sublist with exactly the right amount of slots.
*/
final List<InventorySlotWrapper> backingList;
/**
* This participant ensures that markDirty is only called once for the entire inventory.
*/
final MarkDirtyParticipant markDirtyParticipant = new MarkDirtyParticipant();
InventoryStorageImpl(Inventory inventory) {
super(Collections.emptyList());
this.inventory = inventory;
this.backingList = new ArrayList<>();
}
@Override
public List<SingleSlotStorage<ItemVariant>> getSlots() {
return parts;
}
/**
* Resize slot list to match the current size of the inventory.
*/
private void resizeSlotList() {
int inventorySize = inventory.size();
// If the public-facing list must change...
if (inventorySize != parts.size()) {
// Ensure we have enough wrappers in the backing list.
while (backingList.size() < inventorySize) {
backingList.add(new InventorySlotWrapper(this, backingList.size()));
}
// Update the public-facing list.
parts = Collections.unmodifiableList(backingList.subList(0, inventorySize));
}
}
private InventoryStorage getSidedWrapper(@Nullable Direction direction) {
if (inventory instanceof SidedInventory && direction != null) {
return new SidedInventoryStorageImpl(this, direction);
} else {
return this;
}
}
// Boolean is used to prevent allocation. Null values are not allowed by SnapshotParticipant.
class MarkDirtyParticipant extends SnapshotParticipant<Boolean> {
@Override
protected Boolean createSnapshot() {
return Boolean.TRUE;
}
@Override
protected void readSnapshot(Boolean snapshot) {
}
@Override
protected void onFinalCommit() {
inventory.markDirty();
}
}
}

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.item;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
/**
* Implemented by items to cache the ItemVariant with a null tag inside the Item object directly.
*/
public interface ItemVariantCache {
ItemVariant fabric_getCachedItemVariant();
}

View file

@ -0,0 +1,138 @@
/*
* 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.item;
import java.util.Objects;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import net.minecraft.item.Item;
import net.minecraft.item.Items;
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.item.ItemVariant;
public class ItemVariantImpl implements ItemVariant {
public static ItemVariant of(Item item, @Nullable NbtCompound tag) {
Objects.requireNonNull(item, "Item may not be null.");
// Only tag-less or empty item variants are cached for now.
if (tag == null || item == Items.AIR) {
return ((ItemVariantCache) item).fabric_getCachedItemVariant();
} else {
return new ItemVariantImpl(item, tag);
}
}
private static final Logger LOGGER = LogManager.getLogger("fabric-transfer-api-v1/item");
private final Item item;
private final @Nullable NbtCompound nbt;
private final int hashCode;
public ItemVariantImpl(Item item, NbtCompound nbt) {
this.item = item;
this.nbt = nbt == null ? null : nbt.copy(); // defensive copy
hashCode = Objects.hash(item, nbt);
}
@Override
public Item getObject() {
return item;
}
@Nullable
@Override
public NbtCompound getNbt() {
return nbt;
}
@Override
public boolean isBlank() {
return item == Items.AIR;
}
@Override
public NbtCompound toNbt() {
NbtCompound result = new NbtCompound();
result.putString("item", Registry.ITEM.getId(item).toString());
if (nbt != null) {
result.put("tag", nbt.copy());
}
return result;
}
public static ItemVariant fromNbt(NbtCompound tag) {
try {
Item item = Registry.ITEM.get(new Identifier(tag.getString("item")));
NbtCompound aTag = tag.contains("tag") ? tag.getCompound("tag") : null;
return of(item, aTag);
} catch (RuntimeException runtimeException) {
LOGGER.debug("Tried to load an invalid ItemVariant from NBT: {}", tag, runtimeException);
return ItemVariant.blank();
}
}
@Override
public void toPacket(PacketByteBuf buf) {
if (isBlank()) {
buf.writeBoolean(false);
} else {
buf.writeBoolean(true);
buf.writeVarInt(Item.getRawId(item));
buf.writeNbt(nbt);
}
}
public static ItemVariant fromPacket(PacketByteBuf buf) {
if (!buf.readBoolean()) {
return ItemVariant.blank();
} else {
Item item = Item.byRawId(buf.readVarInt());
NbtCompound nbt = buf.readNbt();
return of(item, nbt);
}
}
@Override
public String toString() {
return "ItemVariantImpl{item=" + item + ", 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;
ItemVariantImpl ItemVariant = (ItemVariantImpl) o;
// fail fast with hash code
return hashCode == ItemVariant.hashCode && item == ItemVariant.item && nbtMatches(ItemVariant.nbt);
}
@Override
public int hashCode() {
return hashCode;
}
}

View file

@ -0,0 +1,123 @@
/*
* 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.item;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.item.PlayerInventoryStorage;
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;
class PlayerInventoryStorageImpl extends InventoryStorageImpl implements PlayerInventoryStorage {
private final DroppedStacks droppedStacks;
private final PlayerEntity player;
PlayerInventoryStorageImpl(PlayerInventory playerInventory) {
super(playerInventory);
this.droppedStacks = new DroppedStacks();
this.player = playerInventory.player;
}
@Override
public void offerOrDrop(ItemVariant resource, long amount, TransactionContext tx) {
StoragePreconditions.notBlankNotNegative(resource, amount);
List<SingleSlotStorage<ItemVariant>> mainSlots = getSlots().subList(0, PlayerInventory.MAIN_SIZE);
// Stack into the main stack first
SingleSlotStorage<ItemVariant> selectedSlot = getSlots().get(player.getInventory().selectedSlot);
if (selectedSlot.getResource().equals(resource)) {
amount -= selectedSlot.insert(resource, amount, tx);
}
// Stack into the offhand stack otherwise
SingleSlotStorage<ItemVariant> offHandSlot = getSlots().get(PlayerInventory.OFF_HAND_SLOT);
if (offHandSlot.getResource().equals(resource)) {
amount -= offHandSlot.insert(resource, amount, tx);
}
// Otherwise insert into the main slots, first iteration tries to stack, second iteration inserts into empty slots.
for (int iteration = 0; iteration < 2; iteration++) {
boolean allowEmptySlots = iteration == 1;
for (SingleSlotStorage<ItemVariant> slot : mainSlots) {
if (!slot.isResourceBlank() || allowEmptySlots) {
amount -= slot.insert(resource, amount, tx);
}
}
}
// Drop leftover in the world on the server side (will be synced by the game with the client).
// Dropping items is server-side only because it involves randomness.
if (amount > 0 && player.world.isClient()) {
droppedStacks.addDrop(resource, amount, tx);
}
}
private class DroppedStacks extends SnapshotParticipant<Integer> {
final List<ItemVariant> droppedKeys = new ArrayList<>();
final List<Long> droppedCounts = new ArrayList<>();
void addDrop(ItemVariant key, long count, TransactionContext transaction) {
updateSnapshots(transaction);
droppedKeys.add(key);
droppedCounts.add(count);
}
@Override
protected Integer createSnapshot() {
return droppedKeys.size();
}
@Override
protected void readSnapshot(Integer snapshot) {
// effectively cancel dropping the stacks
int previousSize = snapshot;
while (droppedKeys.size() > previousSize) {
droppedKeys.remove(droppedKeys.size() - 1);
droppedCounts.remove(droppedCounts.size() - 1);
}
}
@Override
protected void onFinalCommit() {
// actually drop the stacks
for (int i = 0; i < droppedKeys.size(); ++i) {
ItemVariant key = droppedKeys.get(i);
while (droppedCounts.get(i) > 0) {
int dropped = (int) Math.min(key.getItem().getMaxCount(), droppedCounts.get(i));
player.dropStack(key.toStack(dropped));
droppedCounts.set(i, droppedCounts.get(i) - dropped);
}
}
droppedKeys.clear();
droppedCounts.clear();
}
}
}

View file

@ -0,0 +1,77 @@
/*
* 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.item;
import net.minecraft.inventory.SidedInventory;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
/**
* Wrapper around an {@link InventorySlotWrapper}, with additional canInsert and canExtract checks.
*/
class SidedInventorySlotWrapper implements SingleSlotStorage<ItemVariant> {
private final InventorySlotWrapper slotWrapper;
private final SidedInventory sidedInventory;
private final Direction direction;
SidedInventorySlotWrapper(InventorySlotWrapper slotWrapper, SidedInventory sidedInventory, Direction direction) {
this.slotWrapper = slotWrapper;
this.sidedInventory = sidedInventory;
this.direction = direction;
}
@Override
public long insert(ItemVariant resource, long maxAmount, TransactionContext transaction) {
if (!sidedInventory.canInsert(slotWrapper.slot, resource.toStack(), direction)) {
return 0;
} else {
return slotWrapper.insert(resource, maxAmount, transaction);
}
}
@Override
public long extract(ItemVariant resource, long maxAmount, TransactionContext transaction) {
if (!sidedInventory.canExtract(slotWrapper.slot, resource.toStack(), direction)) {
return 0;
} else {
return slotWrapper.extract(resource, maxAmount, transaction);
}
}
@Override
public boolean isResourceBlank() {
return slotWrapper.isResourceBlank();
}
@Override
public ItemVariant getResource() {
return slotWrapper.getResource();
}
@Override
public long getAmount() {
return slotWrapper.getAmount();
}
@Override
public long getCapacity() {
return slotWrapper.getCapacity();
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.item;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import net.minecraft.inventory.SidedInventory;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
/**
* Sidedness-aware wrapper around a {@link InventoryStorageImpl} for sided inventories.
*/
class SidedInventoryStorageImpl extends CombinedStorage<ItemVariant, SingleSlotStorage<ItemVariant>> implements InventoryStorage {
SidedInventoryStorageImpl(InventoryStorageImpl storage, Direction direction) {
super(Collections.unmodifiableList(createWrapperList(storage, direction)));
}
@Override
public List<SingleSlotStorage<ItemVariant>> getSlots() {
return parts;
}
private static List<SingleSlotStorage<ItemVariant>> createWrapperList(InventoryStorageImpl storage, Direction direction) {
SidedInventory inventory = (SidedInventory) storage.inventory;
int[] availableSlots = inventory.getAvailableSlots(direction);
SidedInventorySlotWrapper[] slots = new SidedInventorySlotWrapper[availableSlots.length];
for (int i = 0; i < availableSlots.length; ++i) {
slots[i] = new SidedInventorySlotWrapper(storage.backingList.get(availableSlots[i]), inventory, direction);
}
return Arrays.asList(slots);
}
}

View file

@ -0,0 +1,29 @@
/*
* 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 org.spongepowered.asm.mixin.gen.Accessor;
import net.minecraft.fluid.Fluid;
import net.minecraft.item.BucketItem;
@Mixin(BucketItem.class)
public interface BucketItemAccessor {
@Accessor("fluid")
Fluid fabric_getFluid();
}

View file

@ -0,0 +1,32 @@
/*
* 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 org.spongepowered.asm.mixin.gen.Accessor;
import net.minecraft.inventory.DoubleInventory;
import net.minecraft.inventory.Inventory;
@Mixin(DoubleInventory.class)
public interface DoubleInventoryAccessor {
@Accessor("first")
Inventory fabric_getFirst();
@Accessor("second")
Inventory fabric_getSecond();
}

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.mixin.transfer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import net.minecraft.block.DropperBlock;
import net.minecraft.block.entity.DispenserBlockEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPointerImpl;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
/**
* Allows droppers to insert into ItemVariant storages.
*/
@Mixin(DropperBlock.class)
public class DropperBlockMixin {
@Inject(
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/util/math/BlockPos;offset(Lnet/minecraft/util/math/Direction;)Lnet/minecraft/util/math/BlockPos;"
),
method = "dispense",
locals = LocalCapture.CAPTURE_FAILHARD,
cancellable = true,
allow = 1
)
public void hookDispense(ServerWorld world, BlockPos pos, CallbackInfo ci, BlockPointerImpl blockPointerImpl, DispenserBlockEntity dispenser, int slot, ItemStack stack, Direction direction) {
Storage<ItemVariant> target = ItemStorage.SIDED.find(world, pos.offset(direction), direction.getOpposite());
if (target != null) {
Storage<ItemVariant> source = InventoryStorage.of(dispenser, null).getSlots().get(slot);
if (StorageUtil.move(source, target, k -> true, 1, null) == 1) {
ci.cancel();
}
}
}
}

View file

@ -0,0 +1,36 @@
/*
* 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 org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import net.minecraft.block.entity.HopperBlockEntity;
import net.minecraft.inventory.Inventory;
/**
* Hopper accessors, for use in {@link HopperBlockEntityMixin}.
*/
@Mixin(HopperBlockEntity.class)
public interface HopperBlockEntityAccessor extends Inventory {
@Invoker("setCooldown")
void fabric_callSetCooldown(int cooldown);
@Accessor("lastTickTime")
long fabric_getLastTickTime();
}

View file

@ -0,0 +1,92 @@
/*
* 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.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.block.BlockState;
import net.minecraft.block.HopperBlock;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.Hopper;
import net.minecraft.block.entity.HopperBlockEntity;
import net.minecraft.inventory.Inventory;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
/**
* Allows hoppers to interact with ItemVariant storages.
*/
@Mixin(HopperBlockEntity.class)
public class HopperBlockEntityMixin {
@Inject(
at = @At("HEAD"),
method = "insert(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/inventory/Inventory;)Z",
cancellable = true
)
private static void hookInsert(World world, BlockPos pos, BlockState state, Inventory inventory, CallbackInfoReturnable<Boolean> cir) {
Direction direction = state.get(HopperBlock.FACING);
BlockPos targetPos = pos.offset(direction);
BlockEntity targetBe = world.getBlockEntity(targetPos);
Storage<ItemVariant> target = ItemStorage.SIDED.find(world, targetPos, null, targetBe, direction.getOpposite());
if (target != null) {
cir.setReturnValue(doTransfer(InventoryStorage.of(inventory, direction), target, inventory, targetBe));
}
}
@Inject(
at = @At("HEAD"),
method = "extract(Lnet/minecraft/world/World;Lnet/minecraft/block/entity/Hopper;)Z",
cancellable = true
)
private static void hookExtract(World world, Hopper hopper, CallbackInfoReturnable<Boolean> cir) {
BlockPos sourcePos = new BlockPos(hopper.getHopperX(), hopper.getHopperY() + 1.0D, hopper.getHopperZ());
BlockEntity sourceBe = world.getBlockEntity(sourcePos);
Storage<ItemVariant> source = ItemStorage.SIDED.find(world, sourcePos, null, sourceBe, Direction.DOWN);
if (source != null) {
cir.setReturnValue(doTransfer(source, InventoryStorage.of(hopper, Direction.UP), sourceBe, hopper));
}
}
private static boolean doTransfer(Storage<ItemVariant> from, Storage<ItemVariant> to, @Nullable Object invFrom, @Nullable Object invTo) {
if (invFrom instanceof HopperBlockEntityAccessor hopperFrom && invTo instanceof HopperBlockEntityAccessor hopperTo) {
// Hoppers have some special interactions (see HopperBlockEntity#transfer)
boolean wasEmpty = hopperTo.isEmpty();
boolean moved = StorageUtil.move(from, to, k -> true, 1, null) == 1;
if (moved && wasEmpty && hopperTo.fabric_getLastTickTime() >= hopperFrom.fabric_getLastTickTime()) {
hopperTo.fabric_callSetCooldown(7);
}
return moved;
} else {
return StorageUtil.move(from, to, k -> true, 1, null) == 1;
}
}
}

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 org.spongepowered.asm.mixin.Unique;
import net.minecraft.item.Item;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.impl.transfer.item.ItemVariantCache;
import net.fabricmc.fabric.impl.transfer.item.ItemVariantImpl;
/**
* Cache the ItemVariant with a null tag inside each Item directly.
*/
@Mixin(Item.class)
public class ItemMixin implements ItemVariantCache {
@Unique
private final ItemVariant cachedItemVariant = new ItemVariantImpl((Item) (Object) this, null);
@Override
public ItemVariant fabric_getCachedItemVariant() {
return cachedItemVariant;
}
}

View file

@ -3,6 +3,12 @@
"package": "net.fabricmc.fabric.mixin.transfer",
"compatibilityLevel": "JAVA_8",
"mixins": [
"FluidMixin"
"BucketItemAccessor",
"DoubleInventoryAccessor",
"DropperBlockMixin",
"FluidMixin",
"HopperBlockEntityAccessor",
"HopperBlockEntityMixin",
"ItemMixin"
]
}

View file

@ -5,7 +5,7 @@
"version": "${version}",
"environment": "*",
"license": "Apache-2.0",
"icon": "assets/fabric-api-lookup-api-v1/icon.png",
"icon": "assets/fabric-transfer-api-v1/icon.png",
"contact": {
"homepage": "https://fabricmc.net",
"irc": "irc://irc.esper.net:6667/fabric",
@ -20,7 +20,7 @@
"fabric-api-lookup-api-v1": "*",
"fabric-rendering-fluids-v1": "*"
},
"description": "A common API for the transfer of fluids and other game resources.",
"description": "A common API for the transfer of fluids, items and other game resources.",
"mixins": [
"fabric-transfer-api-v1.mixins.json"
],

View file

@ -0,0 +1,185 @@
/*
* 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.BOTTLE;
import static net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants.BUCKET;
import java.util.List;
import java.util.Objects;
import net.minecraft.fluid.Fluids;
import net.minecraft.inventory.Inventory;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.potion.PotionUtil;
import net.minecraft.potion.Potions;
import net.fabricmc.fabric.api.transfer.v1.context.ContainerItemContext;
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.item.InventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
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.storage.base.ResourceAmount;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
public class FluidItemTests {
public static void run() {
testFluidItemApi();
testWaterPotion();
testSimpleContentsQuery();
}
private static void testFluidItemApi() {
FluidVariant water = FluidVariant.of(Fluids.WATER);
ItemVariant waterBucket = ItemVariant.of(Items.WATER_BUCKET);
Inventory testInventory = new FluidItemTestInventory(ItemStack.EMPTY, new ItemStack(Items.BUCKET), new ItemStack(Items.WATER_BUCKET));
Storage<FluidVariant> slot1Storage = new InventoryContainerItem(testInventory, 1).find(FluidStorage.ITEM);
Storage<FluidVariant> slot2Storage = new InventoryContainerItem(testInventory, 2).find(FluidStorage.ITEM);
if (slot1Storage == null || slot2Storage == null) throw new AssertionError("We should have provided a fluid storage for buckets.");
try (Transaction transaction = Transaction.openOuter()) {
// Test extract.
if (slot2Storage.extract(water, BUCKET, transaction) != BUCKET) throw new AssertionError("Should have extracted from full bucket.");
// Test that an empty bucket was added.
if (!stackEquals(testInventory.getStack(1), Items.BUCKET, 2)) throw new AssertionError("Buckets should have stacked.");
// Test that we can't extract again
if (slot2Storage.extract(water, BUCKET, transaction) != 0) throw new AssertionError("Should not have extracted a second time.");
// Now insert water into slot 1.
if (slot1Storage.insert(water, BUCKET, transaction) != BUCKET) throw new AssertionError("Failed to insert.");
// Check that it filled slot 0.
if (!stackEquals(testInventory.getStack(0), Items.WATER_BUCKET, 1)) throw new AssertionError("Should have filled slot 0.");
// Now we yeet the bucket just because we can.
SingleSlotStorage<ItemVariant> slot0 = InventoryStorage.of(testInventory, null).getSlots().get(0);
if (slot0.extract(waterBucket, 1, transaction) != 1) throw new AssertionError("Failed to yeet bucket.");
// Now insert should fill slot 1 with a bucket.
if (slot1Storage.insert(water, BUCKET, transaction) != BUCKET) throw new AssertionError("Failed to insert.");
// Check inventory contents.
if (!testInventory.getStack(0).isEmpty()) throw new AssertionError("Slot 0 should have been empty.");
if (!stackEquals(testInventory.getStack(1), Items.WATER_BUCKET, 1)) throw new AssertionError("Should have filled slot 1 with a water bucket.");
}
// Check contents after abort
if (!testInventory.getStack(0).isEmpty()) throw new AssertionError("Failed to abort slot 0.");
if (!stackEquals(testInventory.getStack(1), Items.BUCKET, 1)) throw new AssertionError("Failed to abort slot 1.");
if (!stackEquals(testInventory.getStack(2), Items.WATER_BUCKET, 1)) throw new AssertionError("Failed to abort slot 2.");
}
private static boolean stackEquals(ItemStack stack, Item item, int count) {
return stack.getItem() == item && stack.getCount() == count;
}
private static class FluidItemTestInventory extends SimpleInventory {
FluidItemTestInventory(ItemStack... stacks) {
super(stacks);
}
@Override
public boolean isValid(int slot, ItemStack stack) {
return slot != 2; // Forbid insertion into slot 2.
}
}
private static class InventoryContainerItem implements ContainerItemContext {
private final InventoryStorage inventory;
private final SingleSlotStorage<ItemVariant> slot;
InventoryContainerItem(Inventory inv, int slotIndex) {
this.inventory = InventoryStorage.of(inv, null);
this.slot = inventory.getSlots().get(slotIndex);
}
@Override
public SingleSlotStorage<ItemVariant> getMainSlot() {
return slot;
}
@Override
public long insertOverflow(ItemVariant itemVariant, long maxAmount, TransactionContext transactionContext) {
long inserted = 0;
// Try to be smart and stack first!
for (SingleSlotStorage<ItemVariant> slot : inventory.getSlots()) {
if (slot.getResource().equals(itemVariant)) {
inserted += slot.insert(itemVariant, maxAmount - inserted, transactionContext);
}
}
return inserted + inventory.insert(itemVariant, maxAmount - inserted, transactionContext);
}
@Override
public List<SingleSlotStorage<ItemVariant>> getAdditionalSlots() {
return inventory.getSlots();
}
}
private static void testWaterPotion() {
FluidVariant water = FluidVariant.of(Fluids.WATER);
Inventory testInventory = new SimpleInventory(new ItemStack(Items.GLASS_BOTTLE));
// Try to fill empty potion
Storage<FluidVariant> emptyBottleStorage = new InventoryContainerItem(testInventory, 0).find(FluidStorage.ITEM);
try (Transaction transaction = Transaction.openOuter()) {
if (emptyBottleStorage.insert(water, Long.MAX_VALUE, transaction) != BOTTLE) throw new AssertionError("Failed to insert.");
transaction.commit();
}
if (PotionUtil.getPotion(testInventory.getStack(0)) != Potions.WATER) throw new AssertionError("Expected water potion.");
// Try to empty from water potion
Storage<FluidVariant> waterBottleStroage = new InventoryContainerItem(testInventory, 0).find(FluidStorage.ITEM);
try (Transaction transaction = Transaction.openOuter()) {
if (waterBottleStroage.extract(water, Long.MAX_VALUE, transaction) != BOTTLE) throw new AssertionError("Failed to extract.");
transaction.commit();
}
// Make sure extraction nothing is returned for other potions
PotionUtil.setPotion(testInventory.getStack(0), Potions.LUCK);
Storage<FluidVariant> luckyStorage = new InventoryContainerItem(testInventory, 0).find(FluidStorage.ITEM);
if (StorageUtil.findStoredResource(luckyStorage, null) != null) {
throw new AssertionError("Found a resource in an unhandled potion.");
}
}
private static void testSimpleContentsQuery() {
assertEquals(
new ResourceAmount<>(FluidVariant.of(Fluids.WATER), BUCKET),
StorageUtil.findExtractableContent(
ContainerItemContext.withInitial(new ItemStack(Items.WATER_BUCKET)).find(FluidStorage.ITEM),
null
)
);
}
private static void assertEquals(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError(String.format("assertEquals failed%nexpected: %s%n but was: %s", expected, actual));
}
}
}

View file

@ -34,7 +34,8 @@ 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.storage.base.SingleSlotStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
public class FluidTransferTest implements ModInitializer {
@ -61,6 +62,8 @@ public class FluidTransferTest implements ModInitializer {
testFluidStorage();
testTransactionExceptions();
ItemTests.run();
FluidItemTests.run();
}
private static void registerBlock(Block block, String name) {
@ -70,10 +73,15 @@ public class FluidTransferTest implements ModInitializer {
}
private static final FluidVariant TAGGED_WATER, TAGGED_WATER_2, WATER, LAVA;
private static int markDirtyCount = 0;
private static int finalCommitCount = 0;
private static SingleSlotStorage<FluidVariant> createWaterStorage() {
return new SingleVariantStorage<>() {
@Override
protected FluidVariant getBlankVariant() {
return FluidVariant.blank();
}
private static SingleFluidStorage createWaterStorage() {
return new SingleFluidStorage() {
@Override
protected long getCapacity(FluidVariant fluidVariant) {
return BUCKET * 2;
@ -85,8 +93,8 @@ public class FluidTransferTest implements ModInitializer {
}
@Override
protected void markDirty() {
markDirtyCount++;
protected void onFinalCommit() {
finalCommitCount++;
}
};
}
@ -101,7 +109,7 @@ public class FluidTransferTest implements ModInitializer {
}
private static void testFluidStorage() {
SingleFluidStorage waterStorage = createWaterStorage();
SingleSlotStorage<FluidVariant> waterStorage = createWaterStorage();
// Test content
if (!waterStorage.isResourceBlank()) throw new AssertionError("Should have been blank");
@ -156,15 +164,15 @@ public class FluidTransferTest implements ModInitializer {
// 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)");
if (finalCommitCount != 0) throw new AssertionError("Nothing should have called onFinalCommit() 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.");
if (finalCommitCount != 1) throw new AssertionError("onFinalCommit() should have been called exactly once.");
}
private static void insertWaterWithNesting(SingleFluidStorage waterStorage, boolean doOuterCommit) {
private static void insertWaterWithNesting(SingleSlotStorage<FluidVariant> 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");

View file

@ -0,0 +1,188 @@
/*
* 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.stream.IntStream;
import org.jetbrains.annotations.Nullable;
import net.minecraft.inventory.Inventory;
import net.minecraft.inventory.SidedInventory;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.util.math.Direction;
import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
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;
/**
* Tests for the item transfer APIs.
*/
public class ItemTests {
public static void run() {
testInventoryWrappers();
testLimitedStackCountInventory();
testLimitedStackCountItem();
}
private static void testInventoryWrappers() {
ItemVariant emptyBucket = ItemVariant.of(Items.BUCKET);
TestSidedInventory testInventory = new TestSidedInventory();
checkComparatorOutput(testInventory, null);
// Create a few wrappers.
InventoryStorage unsidedWrapper = InventoryStorage.of(testInventory, null);
InventoryStorage downWrapper = InventoryStorage.of(testInventory, Direction.DOWN);
InventoryStorage upWrapper = InventoryStorage.of(testInventory, Direction.UP);
// Make sure querying a new wrapper returns the same one.
if (InventoryStorage.of(testInventory, null) != unsidedWrapper) throw new AssertionError("Wrappers should be ==.");
for (int iter = 0; iter < 2; ++iter) {
// First time, abort.
// Second time, commit.
try (Transaction transaction = Transaction.openOuter()) {
// Insert bucket from down - should fail.
if (downWrapper.insert(emptyBucket, 1, transaction) != 0) throw new AssertionError("Bucket should not have been inserted.");
// Insert bucket unsided - should go in slot 1 (isValid returns false for slot 0).
if (unsidedWrapper.insert(emptyBucket, 1, transaction) != 1) throw new AssertionError("Failed to insert bucket.");
if (!testInventory.getStack(0).isEmpty()) throw new AssertionError("Slot 0 should have been empty.");
if (!stackEquals(testInventory.getStack(1), Items.BUCKET, 1)) throw new AssertionError("Slot 1 should have been a bucket.");
// The bucket should be extractable from any side but the top.
if (!emptyBucket.equals(StorageUtil.findExtractableResource(unsidedWrapper, transaction))) throw new AssertionError("Bucket should be extractable from unsided wrapper.");
if (!emptyBucket.equals(StorageUtil.findExtractableResource(downWrapper, transaction))) throw new AssertionError("Bucket should be extractable from down wrapper.");
if (StorageUtil.findExtractableResource(upWrapper, transaction) != null) throw new AssertionError("Bucket should NOT be extractable from up wrapper.");
if (iter == 1) {
// Commit the second time only.
transaction.commit();
}
}
}
// Check commit.
if (!testInventory.getStack(0).isEmpty()) throw new AssertionError("Slot 0 should have been empty.");
if (!testInventory.getStack(1).isOf(Items.BUCKET) || testInventory.getStack(1).getCount() != 1) throw new AssertionError("Slot 1 should have been a bucket.");
checkComparatorOutput(testInventory, null);
}
private static boolean stackEquals(ItemStack stack, Item item, int count) {
return stack.getItem() == item && stack.getCount() == count;
}
private static class TestSidedInventory extends SimpleInventory implements SidedInventory {
private static final int[] SLOTS = IntStream.range(0, 3).toArray();
TestSidedInventory() {
super(SLOTS.length);
}
@Override
public int[] getAvailableSlots(Direction side) {
return SLOTS;
}
@Override
public boolean isValid(int slot, ItemStack stack) {
return slot != 0 || !stack.isOf(Items.BUCKET); // can't have buckets in slot 0.
}
@Override
public boolean canInsert(int slot, ItemStack stack, @Nullable Direction dir) {
return dir != Direction.DOWN;
}
@Override
public boolean canExtract(int slot, ItemStack stack, Direction dir) {
return dir != Direction.UP;
}
}
/**
* Test insertion when {@link Inventory#getMaxCountPerStack()} is the bottleneck.
*/
private static void testLimitedStackCountInventory() {
ItemVariant diamond = ItemVariant.of(Items.DIAMOND);
LimitedStackCountInventory inventory = new LimitedStackCountInventory(diamond.toStack(), diamond.toStack(), diamond.toStack());
InventoryStorage wrapper = InventoryStorage.of(inventory, null);
// Should only be able to insert 2 diamonds per stack * 3 stacks = 6 diamonds.
try (Transaction transaction = Transaction.openOuter()) {
if (wrapper.insert(diamond, 1000, transaction) != 6) {
throw new AssertionError("Only 6 diamonds should have been inserted.");
}
checkComparatorOutput(inventory, transaction);
}
}
/**
* Test insertion when {@link Item#getMaxCount()} is the bottleneck.
*/
private static void testLimitedStackCountItem() {
ItemVariant diamondPickaxe = ItemVariant.of(Items.DIAMOND_PICKAXE);
LimitedStackCountInventory inventory = new LimitedStackCountInventory(5);
InventoryStorage wrapper = InventoryStorage.of(inventory, null);
// Should only be able to insert 5 pickaxes, as the item limits stack counts to 1.
try (Transaction transaction = Transaction.openOuter()) {
if (wrapper.insert(diamondPickaxe, 1000, transaction) != 5) {
throw new AssertionError("Only 5 pickaxes should have been inserted.");
}
checkComparatorOutput(inventory, transaction);
}
}
private static class LimitedStackCountInventory extends SimpleInventory {
LimitedStackCountInventory(int size) {
super(size);
}
LimitedStackCountInventory(ItemStack... stacks) {
super(stacks);
}
@Override
public int getMaxCountPerStack() {
return 3;
}
}
private static void checkComparatorOutput(Inventory inventory, @Nullable Transaction transaction) {
Storage<ItemVariant> storage = InventoryStorage.of(inventory, null);
int vanillaOutput = ScreenHandler.calculateComparatorOutput(inventory);
int transferApiOutput = StorageUtil.calculateComparatorOutput(storage, transaction);
if (vanillaOutput != transferApiOutput) {
String error = String.format(
"Vanilla and Transfer API comparator outputs should have been identical. Vanilla: %d. Transfer API: %d.",
vanillaOutput,
transferApiOutput
);
throw new AssertionError(error);
}
}
}