From 0d7a4ee070ab3155ea4da1c7cb432172aa0df956 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Tue, 17 Aug 2021 20:08:09 +0200 Subject: [PATCH] Fabric Transfer API: item transfer and fluid-containing items. (#1553) * 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 #1565 * 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 --- build.gradle | 8 +- fabric-api-lookup-api-v1/build.gradle | 2 +- .../api/lookup/v1/block/BlockApiLookup.java | 7 + .../api/lookup/v1/item/ItemApiLookup.java | 8 + .../impl/lookup/block/BlockApiLookupImpl.java | 1 + .../impl/lookup/item/ItemApiLookupImpl.java | 6 + fabric-transfer-api-v1/README.md | 22 +- fabric-transfer-api-v1/build.gradle | 2 +- .../v1/context/ContainerItemContext.java | 242 ++++++++++++++++++ .../api/transfer/v1/fluid/FluidStorage.java | 104 ++++++++ .../v1/fluid/base/EmptyItemFluidStorage.java | 129 ++++++++++ .../v1/fluid/base/FullItemFluidStorage.java | 136 ++++++++++ .../v1/fluid/base/SingleFluidStorage.java | 13 +- .../transfer/v1/item/InventoryStorage.java | 74 ++++++ .../api/transfer/v1/item/ItemStorage.java | 106 ++++++++ .../api/transfer/v1/item/ItemVariant.java | 121 +++++++++ .../v1/item/PlayerInventoryStorage.java | 79 ++++++ .../v1/item/base/SingleStackStorage.java | 164 ++++++++++++ .../fabric/api/transfer/v1/package-info.java | 26 +- .../api/transfer/v1/storage/Storage.java | 1 + .../api/transfer/v1/storage/StorageUtil.java | 72 +++++- .../v1/storage/base/BlankVariantView.java | 75 ++++++ .../v1/storage/base/CombinedStorage.java | 2 +- .../v1/storage/base/SingleVariantStorage.java | 145 +++++++++++ .../transfer/v1/transaction/Transaction.java | 8 + .../transaction/base/SnapshotParticipant.java | 6 +- .../InitialContentsContainerItemContext.java | 63 +++++ .../context/PlayerContainerItemContext.java | 64 +++++ .../SingleSlotContainerItemContext.java | 49 ++++ .../impl/transfer/fluid/CauldronStorage.java | 3 +- .../transfer/fluid/CombinedProvidersImpl.java | 100 ++++++++ .../transfer/fluid/EmptyBucketStorage.java | 72 ++++++ .../transfer/fluid/WaterPotionStorage.java | 117 +++++++++ .../impl/transfer/item/ComposterWrapper.java | 203 +++++++++++++++ .../impl/transfer/item/CursorSlotWrapper.java | 53 ++++ .../transfer/item/InventorySlotWrapper.java | 69 +++++ .../transfer/item/InventoryStorageImpl.java | 131 ++++++++++ .../impl/transfer/item/ItemVariantCache.java | 26 ++ .../impl/transfer/item/ItemVariantImpl.java | 138 ++++++++++ .../item/PlayerInventoryStorageImpl.java | 123 +++++++++ .../item/SidedInventorySlotWrapper.java | 77 ++++++ .../item/SidedInventoryStorageImpl.java | 55 ++++ .../mixin/transfer/BucketItemAccessor.java | 29 +++ .../transfer/DoubleInventoryAccessor.java | 32 +++ .../mixin/transfer/DropperBlockMixin.java | 65 +++++ .../transfer/HopperBlockEntityAccessor.java | 36 +++ .../transfer/HopperBlockEntityMixin.java | 92 +++++++ .../fabric/mixin/transfer/ItemMixin.java | 40 +++ .../fabric-transfer-api-v1/icon.png | Bin .../fabric-transfer-api-v1.mixins.json | 8 +- .../src/main/resources/fabric.mod.json | 4 +- .../test/transfer/fluid/FluidItemTests.java | 185 +++++++++++++ .../transfer/fluid/FluidTransferTest.java | 28 +- .../fabric/test/transfer/fluid/ItemTests.java | 188 ++++++++++++++ 54 files changed, 3564 insertions(+), 45 deletions(-) create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/EmptyItemFluidStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/FullItemFluidStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemVariant.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/PlayerInventoryStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleStackStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/BlankVariantView.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/InitialContentsContainerItemContext.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/PlayerContainerItemContext.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/SingleSlotContainerItemContext.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CombinedProvidersImpl.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/EmptyBucketStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/WaterPotionStorage.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/CursorSlotWrapper.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventorySlotWrapper.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventoryStorageImpl.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantCache.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantImpl.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/PlayerInventoryStorageImpl.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventorySlotWrapper.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventoryStorageImpl.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/BucketItemAccessor.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DoubleInventoryAccessor.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DropperBlockMixin.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityAccessor.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java create mode 100644 fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/ItemMixin.java rename fabric-transfer-api-v1/src/main/resources/{ => assets}/fabric-transfer-api-v1/icon.png (100%) create mode 100644 fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidItemTests.java create mode 100644 fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/ItemTests.java diff --git a/build.gradle b/build.gradle index 03e94ac79..c1ecd0cea 100644 --- a/build.gradle +++ b/build.gradle @@ -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 + } } } } diff --git a/fabric-api-lookup-api-v1/build.gradle b/fabric-api-lookup-api-v1/build.gradle index cdc889cab..6b1803ad4 100644 --- a/fabric-api-lookup-api-v1/build.gradle +++ b/fabric-api-lookup-api-v1/build.gradle @@ -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', diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java index 25c2fe5a5..e16077db6 100644 --- a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java @@ -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> { /** diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/item/ItemApiLookup.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/item/ItemApiLookup.java index b1e0418f0..5c29b03c0 100644 --- a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/item/ItemApiLookup.java +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/item/ItemApiLookup.java @@ -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> { /** diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java index 1c9627a20..c71406e16 100644 --- a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java @@ -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); diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/item/ItemApiLookupImpl.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/item/ItemApiLookupImpl.java index dbd7b8397..b4c44701f 100644 --- a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/item/ItemApiLookupImpl.java +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/item/ItemApiLookupImpl.java @@ -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); + } } diff --git a/fabric-transfer-api-v1/README.md b/fabric-transfer-api-v1/README.md index 674366490..0dd515ede 100644 --- a/fabric-transfer-api-v1/README.md +++ b/fabric-transfer-api-v1/README.md @@ -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. diff --git a/fabric-transfer-api-v1/build.gradle b/fabric-transfer-api-v1/build.gradle index ec7125c07..f2d1b7a48 100644 --- a/fabric-transfer-api-v1/build.gradle +++ b/fabric-transfer-api-v1/build.gradle @@ -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', diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java new file mode 100644 index 000000000..d02130e62 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/context/ContainerItemContext.java @@ -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(); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java index aacfe18d2..32b8e022c 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/FluidStorage.java @@ -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<FluidVariant>} 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); } } diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/EmptyItemFluidStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/EmptyItemFluidStorage.java new file mode 100644 index 000000000..87a35c935 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/EmptyItemFluidStorage.java @@ -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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/FullItemFluidStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/FullItemFluidStorage.java new file mode 100644 index 000000000..a3552dfb1 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/FullItemFluidStorage.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java index 588dc468a..ef915c154 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/fluid/base/SingleFluidStorage.java @@ -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; diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java new file mode 100644 index 000000000..1612ea3ec --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/InventoryStorage.java @@ -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(); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java new file mode 100644 index 000000000..85eec4ff2 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemStorage.java @@ -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<ItemVariant>} 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; + }); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemVariant.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemVariant.java new file mode 100644 index 000000000..e7c29f2b0 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/ItemVariant.java @@ -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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/PlayerInventoryStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/PlayerInventoryStorage.java new file mode 100644 index 000000000..b827af23d --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/PlayerInventoryStorage.java @@ -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); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleStackStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleStackStorage.java new file mode 100644 index 000000000..e29936a4d --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/item/base/SingleStackStorage.java @@ -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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/package-info.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/package-info.java index 8c9ff62d9..4a5d7a3ea 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/package-info.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/package-info.java @@ -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; diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java index 1c38def4e..318021466 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/Storage.java @@ -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. diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java index 460bdc195..2a8eff654 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/StorageUtil.java @@ -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); + } } diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/BlankVariantView.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/BlankVariantView.java new file mode 100644 index 000000000..f4c4eca84 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/BlankVariantView.java @@ -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; + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedStorage.java index f0a18eae4..2a71ff98f 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/CombinedStorage.java @@ -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; diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java new file mode 100644 index 000000000..ba3badc50 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/storage/base/SingleVariantStorage.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/Transaction.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/Transaction.java index 8ce71d7c0..ff3f6110f 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/Transaction.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/Transaction.java @@ -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. diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/base/SnapshotParticipant.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/base/SnapshotParticipant.java index 23d49e6a2..6851048f2 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/base/SnapshotParticipant.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/api/transfer/v1/transaction/base/SnapshotParticipant.java @@ -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(); diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/InitialContentsContainerItemContext.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/InitialContentsContainerItemContext.java new file mode 100644 index 000000000..cd8599956 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/InitialContentsContainerItemContext.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/PlayerContainerItemContext.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/PlayerContainerItemContext.java new file mode 100644 index 000000000..739157432 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/PlayerContainerItemContext.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/SingleSlotContainerItemContext.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/SingleSlotContainerItemContext.java new file mode 100644 index 000000000..60c3650b0 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/context/SingleSlotContainerItemContext.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CauldronStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CauldronStorage.java index 3a04de685..ff467095a 100644 --- a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CauldronStorage.java +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CauldronStorage.java @@ -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; diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CombinedProvidersImpl.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CombinedProvidersImpl.java new file mode 100644 index 000000000..54ca2481d --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/CombinedProvidersImpl.java @@ -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); + } + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/EmptyBucketStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/EmptyBucketStorage.java new file mode 100644 index 000000000..50963272e --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/EmptyBucketStorage.java @@ -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 <-> 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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/WaterPotionStorage.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/WaterPotionStorage.java new file mode 100644 index 000000000..566fb7a66 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/fluid/WaterPotionStorage.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java new file mode 100644 index 000000000..c92299653 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ComposterWrapper.java @@ -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; + } + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/CursorSlotWrapper.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/CursorSlotWrapper.java new file mode 100644 index 000000000..d2609b8a8 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/CursorSlotWrapper.java @@ -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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventorySlotWrapper.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventorySlotWrapper.java new file mode 100644 index 000000000..2d46f0b98 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventorySlotWrapper.java @@ -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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventoryStorageImpl.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventoryStorageImpl.java new file mode 100644 index 000000000..e569a2207 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/InventoryStorageImpl.java @@ -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(); + } + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantCache.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantCache.java new file mode 100644 index 000000000..fef26b1f5 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantCache.java @@ -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(); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantImpl.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantImpl.java new file mode 100644 index 000000000..2b8049ad9 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/ItemVariantImpl.java @@ -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; + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/PlayerInventoryStorageImpl.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/PlayerInventoryStorageImpl.java new file mode 100644 index 000000000..fe4ac3faa --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/PlayerInventoryStorageImpl.java @@ -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(); + } + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventorySlotWrapper.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventorySlotWrapper.java new file mode 100644 index 000000000..f3a9f5d49 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventorySlotWrapper.java @@ -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(); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventoryStorageImpl.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventoryStorageImpl.java new file mode 100644 index 000000000..9519b40ed --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/impl/transfer/item/SidedInventoryStorageImpl.java @@ -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); + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/BucketItemAccessor.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/BucketItemAccessor.java new file mode 100644 index 000000000..2a81f3e57 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/BucketItemAccessor.java @@ -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(); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DoubleInventoryAccessor.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DoubleInventoryAccessor.java new file mode 100644 index 000000000..43711f25d --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DoubleInventoryAccessor.java @@ -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(); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DropperBlockMixin.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DropperBlockMixin.java new file mode 100644 index 000000000..a5c94c200 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/DropperBlockMixin.java @@ -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(); + } + } + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityAccessor.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityAccessor.java new file mode 100644 index 000000000..a4784fbb8 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityAccessor.java @@ -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(); +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java new file mode 100644 index 000000000..c48a3cc75 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/HopperBlockEntityMixin.java @@ -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; + } + } +} diff --git a/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/ItemMixin.java b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/ItemMixin.java new file mode 100644 index 000000000..c05715ad3 --- /dev/null +++ b/fabric-transfer-api-v1/src/main/java/net/fabricmc/fabric/mixin/transfer/ItemMixin.java @@ -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; + } +} diff --git a/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1/icon.png b/fabric-transfer-api-v1/src/main/resources/assets/fabric-transfer-api-v1/icon.png similarity index 100% rename from fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1/icon.png rename to fabric-transfer-api-v1/src/main/resources/assets/fabric-transfer-api-v1/icon.png diff --git a/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json b/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json index 5ead6181e..becd80534 100644 --- a/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json +++ b/fabric-transfer-api-v1/src/main/resources/fabric-transfer-api-v1.mixins.json @@ -3,6 +3,12 @@ "package": "net.fabricmc.fabric.mixin.transfer", "compatibilityLevel": "JAVA_8", "mixins": [ - "FluidMixin" + "BucketItemAccessor", + "DoubleInventoryAccessor", + "DropperBlockMixin", + "FluidMixin", + "HopperBlockEntityAccessor", + "HopperBlockEntityMixin", + "ItemMixin" ] } diff --git a/fabric-transfer-api-v1/src/main/resources/fabric.mod.json b/fabric-transfer-api-v1/src/main/resources/fabric.mod.json index 5e85b90b8..5fb9be279 100644 --- a/fabric-transfer-api-v1/src/main/resources/fabric.mod.json +++ b/fabric-transfer-api-v1/src/main/resources/fabric.mod.json @@ -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" ], diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidItemTests.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidItemTests.java new file mode 100644 index 000000000..55687ea3b --- /dev/null +++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidItemTests.java @@ -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)); + } + } +} diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidTransferTest.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidTransferTest.java index 813f40938..8675d8cf8 100644 --- a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidTransferTest.java +++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/FluidTransferTest.java @@ -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"); diff --git a/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/ItemTests.java b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/ItemTests.java new file mode 100644 index 000000000..b5d817acc --- /dev/null +++ b/fabric-transfer-api-v1/src/testmod/java/net/fabricmc/fabric/test/transfer/fluid/ItemTests.java @@ -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); + } + } +}