From d8cf4e5a102ee23ddd838975ecddf5cf1bbffa2c Mon Sep 17 00:00:00 2001
From: AlphaMode <26313415+alphamode@users.noreply.github.com>
Date: Sun, 20 Nov 2022 07:25:23 -0600
Subject: [PATCH] Support stack aware recipe remainders (#2556)

* Support stack aware recipe remainders

* Fix checkstyle

* Remove all overwrites

* Add FabricItemStack and make RecipeRemainderHandler thread safe

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java

Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>

* Remove hasRecipeRemainder, Update test mod and remove unneeded mixins

* Update fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java

Co-authored-by: Salvatore Peluso <info@devpelux.xyz>

* Avoid copying the ItemStack

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java

Co-authored-by: Salvatore Peluso <info@devpelux.xyz>

* Sneakily change duplicate keybinding to a less used key

* make everything thread safe and improve AbstractFurnaceBlockEntityMixin

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java

Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: Salvatore Peluso <info@devpelux.xyz>

* clear thread local and change field prefix

* forgot the allow

* Update fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java

Co-authored-by: Salvatore Peluso <info@devpelux.xyz>

* Update fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java

Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>

* Add FurnaceGameTest

* Change test keybind back to LShift

* Fix brewing stand remainder and fix nitpicks

* add code example to remainder javadoc

* Fixed and reformatted docs, changed recipe mixin behavior to store the remainder stack instead of the original stack, refactoring.

* Added gametests for brewing stand and recipe mixins, fixed furnace gametest compairing stacks with themselves.

* Use (0,1,0) position for game tests

* Review changes

Co-authored-by: apple502j <33279053+apple502j@users.noreply.github.com>
Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>
Co-authored-by: Salvatore Peluso <info@devpelux.xyz>
Co-authored-by: modmuss50 <modmuss50@gmail.com>
(cherry picked from commit fa140d597616a8dee53622bce701a5c0ac8810b9)
---
 fabric-item-api-v1/build.gradle               |   4 +
 .../fabric/api/item/v1/FabricItem.java        |  32 ++++
 .../fabric/api/item/v1/FabricItemStack.java   |  39 +++++
 .../impl/item/RecipeRemainderHandler.java     |  26 +++
 .../item/AbstractFurnaceBlockEntityMixin.java |  50 ++++++
 .../item/BrewingStandBlockEntityMixin.java    |  59 +++++++
 .../fabric/mixin/item/ItemStackMixin.java     |   3 +-
 .../fabric/mixin/item/RecipeMixin.java        |  52 ++++++
 .../resources/fabric-item-api-v1.mixins.json  |   7 +-
 .../src/main/resources/fabric.mod.json        |   3 +-
 .../fabric/test/item/CustomDamageTest.java    |  22 ++-
 .../item/gametest/BrewingStandGameTest.java   | 149 ++++++++++++++++++
 .../test/item/gametest/FurnaceGameTest.java   | 122 ++++++++++++++
 .../test/item/gametest/RecipeGameTest.java    | 144 +++++++++++++++++
 .../mixin/BrewingRecipeRegistryAccessor.java  |  32 ++++
 .../recipes/weird_pickaxe.json                |  15 ++
 .../fabric-item-api-tests-v1.mixins.json      |  11 ++
 .../src/testmod/resources/fabric.mod.json     |  10 +-
 18 files changed, 774 insertions(+), 6 deletions(-)
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java
 create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java
 create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java
 create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java
 create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java
 create mode 100644 fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json
 create mode 100644 fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json

diff --git a/fabric-item-api-v1/build.gradle b/fabric-item-api-v1/build.gradle
index 692eb5ead..1c1ca60c1 100644
--- a/fabric-item-api-v1/build.gradle
+++ b/fabric-item-api-v1/build.gradle
@@ -4,3 +4,7 @@ version = getSubprojectVersion(project)
 moduleDependencies(project, [
 		'fabric-api-base'
 ])
+
+dependencies {
+	testmodImplementation project(path: ':fabric-content-registries-v0', configuration: 'namedElements')
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java
index 547cb644c..847dc8dc4 100644
--- a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItem.java
@@ -91,4 +91,36 @@ public interface FabricItem {
 	default boolean isSuitableFor(ItemStack stack, BlockState state) {
 		return ((Item) this).isSuitableFor(state);
 	}
+
+	/**
+	 * Returns a leftover item stack after {@code stack} is consumed in a recipe.
+	 * (This is also known as "recipe remainder".)
+	 * For example, using a lava bucket in a furnace as fuel will leave an empty bucket.
+	 *
+	 * <p>Here is an example for a recipe remainder that increments the item's damage.
+	 *
+	 * <pre>
+	 *  if (stack.getDamage() < stack.getMaxDamage() - 1) {
+	 *  	ItemStack moreDamaged = stack.copy();
+	 *  	moreDamaged.setDamage(stack.getDamage() + 1);
+	 *  	return moreDamaged;
+	 *  }
+	 *
+	 *  return ItemStack.EMPTY;
+	 * </pre>
+	 *
+	 *
+	 * <p>This is a stack-aware version of {@link Item#getRecipeRemainder()}.
+	 *
+	 * <p>Note that simple item remainders can also be set via {@link Item.Settings#recipeRemainder(Item)}.
+	 *
+	 * <p>If you want to get a remainder for a stack,
+	 * is recommended to use the stack version of this method: {@link FabricItemStack#getRecipeRemainder()}.
+	 *
+	 * @param stack the consumed {@link ItemStack}
+	 * @return the leftover item stack
+	 */
+	default ItemStack getRecipeRemainder(ItemStack stack) {
+		return ((Item) this).hasRecipeRemainder() ? ((Item) this).getRecipeRemainder().getDefaultStack() : ItemStack.EMPTY;
+	}
 }
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java
new file mode 100644
index 000000000..0c646a6aa
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/FabricItemStack.java
@@ -0,0 +1,39 @@
+/*
+ * 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.item.v1;
+
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+
+/*
+ * Fabric-provided extensions for {@link ItemStack}.
+ * This interface is automatically implemented on all item stacks via Mixin and interface injection.
+ */
+public interface FabricItemStack {
+	/**
+	 * Return a leftover item for use in recipes.
+	 *
+	 * <p>See {@link FabricItem#getRecipeRemainder(ItemStack)} for a more in depth description.
+	 *
+	 * <p>Stack-aware version of {@link Item#getRecipeRemainder()}.
+	 *
+	 * @return the leftover item
+	 */
+	default ItemStack getRecipeRemainder() {
+		return ((ItemStack) this).getItem().getRecipeRemainder((ItemStack) this);
+	}
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.java
new file mode 100644
index 000000000..07c262795
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/RecipeRemainderHandler.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.item;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.item.ItemStack;
+
+@ApiStatus.Internal
+public class RecipeRemainderHandler {
+	public static final ThreadLocal<ItemStack> REMAINDER_STACK = new ThreadLocal<>();
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java
new file mode 100644
index 000000000..b881e0643
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/AbstractFurnaceBlockEntityMixin.java
@@ -0,0 +1,50 @@
+/*
+ * 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.item;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyArg;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.AbstractFurnaceBlockEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.recipe.Recipe;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+@Mixin(AbstractFurnaceBlockEntity.class)
+public abstract class AbstractFurnaceBlockEntityMixin {
+	@Unique
+	private static final ThreadLocal<ItemStack> REMAINDER_STACK = new ThreadLocal<>();
+
+	@Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItem()Lnet/minecraft/item/Item;"), locals = LocalCapture.CAPTURE_FAILHARD, allow = 1)
+	private static void getStackRemainder(World world, BlockPos pos, BlockState state, AbstractFurnaceBlockEntity blockEntity, CallbackInfo ci, boolean bl, boolean bl2, ItemStack itemStack, Recipe recipe, int i) {
+		REMAINDER_STACK.set(itemStack.getRecipeRemainder());
+	}
+
+	@ModifyArg(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/DefaultedList;set(ILjava/lang/Object;)Ljava/lang/Object;"), index = 1, allow = 1)
+	private static <E> E setStackRemainder(E element) {
+		E remainder = (E) REMAINDER_STACK.get();
+		REMAINDER_STACK.remove();
+		return remainder;
+	}
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java
new file mode 100644
index 000000000..fdc37fcb1
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/BrewingStandBlockEntityMixin.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.item;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+import net.minecraft.block.entity.BrewingStandBlockEntity;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.collection.DefaultedList;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+@Mixin(BrewingStandBlockEntity.class)
+public class BrewingStandBlockEntityMixin {
+	@Unique
+	private static final ThreadLocal<ItemStack> REMAINDER_STACK = new ThreadLocal<>();
+
+	@Inject(method = "craft", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;decrement(I)V"), locals = LocalCapture.CAPTURE_FAILHARD)
+	private static void captureItemStack(World world, BlockPos pos, DefaultedList<ItemStack> slots, CallbackInfo ci, ItemStack itemStack) {
+		REMAINDER_STACK.set(itemStack.getRecipeRemainder());
+	}
+
+	@Redirect(method = "craft", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/Item;hasRecipeRemainder()Z"))
+	private static boolean hasStackRecipeRemainder(Item instance) {
+		return !REMAINDER_STACK.get().isEmpty();
+	}
+
+	/**
+	 * Injected after the {@link Item#getRecipeRemainder} to replace the old remainder with are new one.
+	 */
+	@ModifyVariable(method = "craft", at = @At(value = "STORE"), index = 4)
+	private static ItemStack createStackRecipeRemainder(ItemStack old) {
+		ItemStack remainder = REMAINDER_STACK.get();
+		REMAINDER_STACK.remove();
+		return remainder;
+	}
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java
index eb88e7380..08424e4ab 100644
--- a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemStackMixin.java
@@ -37,12 +37,13 @@ import net.minecraft.entity.attribute.EntityAttributeModifier;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
 
+import net.fabricmc.fabric.api.item.v1.FabricItemStack;
 import net.fabricmc.fabric.api.item.v1.CustomDamageHandler;
 import net.fabricmc.fabric.api.item.v1.ModifyItemAttributeModifiersCallback;
 import net.fabricmc.fabric.impl.item.ItemExtensions;
 
 @Mixin(ItemStack.class)
-public abstract class ItemStackMixin {
+public abstract class ItemStackMixin implements FabricItemStack {
 	@Shadow public abstract Item getItem();
 
 	@Unique
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java
new file mode 100644
index 000000000..95a73b109
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RecipeMixin.java
@@ -0,0 +1,52 @@
+/*
+ * 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.item;
+
+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.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.recipe.Recipe;
+import net.minecraft.util.collection.DefaultedList;
+
+import net.fabricmc.fabric.impl.item.RecipeRemainderHandler;
+
+@Mixin(Recipe.class)
+public interface RecipeMixin<C extends Inventory> {
+	@Inject(method = "getRemainder", at = @At(value = "INVOKE", target = "Lnet/minecraft/inventory/Inventory;getStack(I)Lnet/minecraft/item/ItemStack;"), locals = LocalCapture.CAPTURE_FAILHARD)
+	default void captureStack(C inventory, CallbackInfoReturnable<DefaultedList<ItemStack>> cir, DefaultedList<ItemStack> defaultedList, int i) {
+		RecipeRemainderHandler.REMAINDER_STACK.set(inventory.getStack(i).getRecipeRemainder());
+	}
+
+	@Redirect(method = "getRemainder", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/Item;hasRecipeRemainder()Z"))
+	private boolean hasStackRemainder(Item instance) {
+		return !RecipeRemainderHandler.REMAINDER_STACK.get().isEmpty();
+	}
+
+	@Redirect(method = "getRemainder", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/collection/DefaultedList;set(ILjava/lang/Object;)Ljava/lang/Object;"))
+	private Object getStackRemainder(DefaultedList<ItemStack> inventory, int index, Object element) {
+		Object remainder = inventory.set(index, RecipeRemainderHandler.REMAINDER_STACK.get());
+		RecipeRemainderHandler.REMAINDER_STACK.remove();
+		return remainder;
+	}
+}
diff --git a/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json b/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json
index 69d431532..aeb3a0882 100644
--- a/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json
+++ b/fabric-item-api-v1/src/main/resources/fabric-item-api-v1.mixins.json
@@ -3,10 +3,13 @@
   "package": "net.fabricmc.fabric.mixin.item",
   "compatibilityLevel": "JAVA_16",
   "mixins": [
-    "ItemStackMixin",
+    "AbstractFurnaceBlockEntityMixin",
+    "ArmorItemMixin",
+    "BrewingStandBlockEntityMixin",
     "ItemMixin",
+    "ItemStackMixin",
     "LivingEntityMixin",
-    "ArmorItemMixin"
+    "RecipeMixin"
   ],
   "client": [
     "client.ClientPlayerInteractionManagerMixin",
diff --git a/fabric-item-api-v1/src/main/resources/fabric.mod.json b/fabric-item-api-v1/src/main/resources/fabric.mod.json
index 7798a4d0d..cec709c7b 100644
--- a/fabric-item-api-v1/src/main/resources/fabric.mod.json
+++ b/fabric-item-api-v1/src/main/resources/fabric.mod.json
@@ -26,7 +26,8 @@
   "custom": {
     "fabric-api:module-lifecycle": "stable",
     "loom:injected_interfaces": {
-      "net/minecraft/class_1792": ["net/fabricmc/fabric/api/item/v1/FabricItem"]
+      "net/minecraft/class_1792": ["net/fabricmc/fabric/api/item/v1/FabricItem"],
+      "net/minecraft/class_1799": ["net/fabricmc/fabric/api/item/v1/FabricItemStack"]
     }
   }
 }
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java
index d7d83f4dd..ebbb628cd 100644
--- a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/CustomDamageTest.java
@@ -16,10 +16,12 @@
 
 package net.fabricmc.fabric.test.item;
 
+import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
 import net.minecraft.item.PickaxeItem;
 import net.minecraft.item.ToolMaterials;
 import net.minecraft.nbt.NbtCompound;
+import net.minecraft.potion.Potions;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.registry.Registry;
@@ -27,11 +29,17 @@ import net.minecraft.util.registry.Registry;
 import net.fabricmc.api.ModInitializer;
 import net.fabricmc.fabric.api.item.v1.CustomDamageHandler;
 import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
+import net.fabricmc.fabric.api.registry.FuelRegistry;
+import net.fabricmc.fabric.test.item.mixin.BrewingRecipeRegistryAccessor;
 
 public class CustomDamageTest implements ModInitializer {
+	public static final Item WEIRD_PICK = new WeirdPick();
+
 	@Override
 	public void onInitialize() {
-		Registry.register(Registry.ITEM, new Identifier("fabric-item-api-v1-testmod", "weird_pickaxe"), new WeirdPick());
+		Registry.register(Registry.ITEM, new Identifier("fabric-item-api-v1-testmod", "weird_pickaxe"), WEIRD_PICK);
+		FuelRegistry.INSTANCE.add(WEIRD_PICK, 200);
+		BrewingRecipeRegistryAccessor.callRegisterPotionRecipe(Potions.WATER, WEIRD_PICK, Potions.AWKWARD);
 	}
 
 	public static final CustomDamageHandler WEIRD_DAMAGE_HANDLER = (stack, amount, entity, breakCallback) -> {
@@ -55,5 +63,17 @@ public class CustomDamageTest implements ModInitializer {
 			int v = stack.getOrCreateNbt().getInt("weird");
 			return super.getName(stack).shallowCopy().append(" (Weird Value: " + v + ")");
 		}
+
+		@Override
+		public ItemStack getRecipeRemainder(ItemStack stack) {
+			if (stack.getDamage() < stack.getMaxDamage() - 1) {
+				ItemStack moreDamaged = stack.copy();
+				moreDamaged.setCount(1);
+				moreDamaged.setDamage(stack.getDamage() + 1);
+				return moreDamaged;
+			}
+
+			return ItemStack.EMPTY;
+		}
 	}
 }
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java
new file mode 100644
index 000000000..bf2cfc849
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/BrewingStandGameTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.item.gametest;
+
+import java.util.Objects;
+
+import net.minecraft.block.Blocks;
+import net.minecraft.block.entity.BrewingStandBlockEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.potion.PotionUtil;
+import net.minecraft.potion.Potions;
+import net.minecraft.test.GameTest;
+import net.minecraft.test.TestContext;
+import net.minecraft.util.math.BlockPos;
+
+import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
+import net.fabricmc.fabric.test.item.CustomDamageTest;
+
+public class BrewingStandGameTest implements FabricGameTest {
+	private static final int BREWING_TIME = 800;
+	private static final BlockPos POS = new BlockPos(0, 1, 0);
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void basicBrewing(TestContext context) {
+		context.setBlockState(POS, Blocks.BREWING_STAND);
+		BrewingStandBlockEntity blockEntity = (BrewingStandBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS));
+
+		loadFuel(blockEntity, context);
+
+		prepareForBrewing(blockEntity, new ItemStack(Items.NETHER_WART, 8),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER));
+
+		brew(blockEntity, context);
+		assertInventory(blockEntity, "Testing vanilla brewing.",
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				new ItemStack(Items.NETHER_WART, 7),
+				ItemStack.EMPTY);
+
+		context.complete();
+	}
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void vanillaRemainderTest(TestContext context) {
+		context.setBlockState(POS, Blocks.BREWING_STAND);
+		BrewingStandBlockEntity blockEntity = (BrewingStandBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS));
+
+		loadFuel(blockEntity, context);
+
+		prepareForBrewing(blockEntity, new ItemStack(Items.DRAGON_BREATH),
+				PotionUtil.setPotion(new ItemStack(Items.SPLASH_POTION), Potions.AWKWARD));
+
+		brew(blockEntity, context);
+		assertInventory(blockEntity, "Testing vanilla brewing recipe remainder.",
+				PotionUtil.setPotion(new ItemStack(Items.LINGERING_POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.LINGERING_POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.LINGERING_POTION), Potions.AWKWARD),
+				new ItemStack(Items.GLASS_BOTTLE),
+				ItemStack.EMPTY);
+
+		context.complete();
+	}
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void fabricRemainderTest(TestContext context) {
+		context.setBlockState(POS, Blocks.BREWING_STAND);
+		BrewingStandBlockEntity blockEntity = (BrewingStandBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS));
+
+		loadFuel(blockEntity, context);
+
+		prepareForBrewing(blockEntity, new ItemStack(CustomDamageTest.WEIRD_PICK),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER));
+
+		brew(blockEntity, context);
+		assertInventory(blockEntity, "Testing fabric brewing recipe remainder.",
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 1),
+				ItemStack.EMPTY);
+
+		prepareForBrewing(blockEntity, RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 10),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER));
+
+		brew(blockEntity, context);
+		assertInventory(blockEntity, "Testing fabric brewing recipe remainder.",
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 11),
+				ItemStack.EMPTY);
+
+		prepareForBrewing(blockEntity, RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 31),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER));
+
+		brew(blockEntity, context);
+		assertInventory(blockEntity, "Testing fabric brewing recipe remainder.",
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.AWKWARD),
+				ItemStack.EMPTY,
+				ItemStack.EMPTY);
+
+		context.complete();
+	}
+
+	private void prepareForBrewing(BrewingStandBlockEntity blockEntity, ItemStack ingredient, ItemStack potion) {
+		blockEntity.setStack(0, potion.copy());
+		blockEntity.setStack(1, potion.copy());
+		blockEntity.setStack(2, potion.copy());
+		blockEntity.setStack(3, ingredient);
+	}
+
+	private void assertInventory(BrewingStandBlockEntity blockEntity, String extraErrorInfo, ItemStack... stacks) {
+		for (int i = 0; i < stacks.length; i++) {
+			ItemStack currentStack = blockEntity.getStack(i);
+			ItemStack expectedStack = stacks[i];
+
+			RecipeGameTest.assertStacks(currentStack, expectedStack, extraErrorInfo);
+		}
+	}
+
+	private void loadFuel(BrewingStandBlockEntity blockEntity, TestContext context) {
+		blockEntity.setStack(4, new ItemStack(Items.BLAZE_POWDER));
+		BrewingStandBlockEntity.tick(context.getWorld(), POS, context.getBlockState(POS), blockEntity);
+	}
+
+	private void brew(BrewingStandBlockEntity blockEntity, TestContext context) {
+		for (int i = 0; i < BREWING_TIME; i++) {
+			BrewingStandBlockEntity.tick(context.getWorld(), POS, context.getBlockState(POS), blockEntity);
+		}
+	}
+}
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java
new file mode 100644
index 000000000..e9eac9f15
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/FurnaceGameTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.item.gametest;
+
+import java.util.Objects;
+
+import net.minecraft.block.Blocks;
+import net.minecraft.block.entity.AbstractFurnaceBlockEntity;
+import net.minecraft.block.entity.FurnaceBlockEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.test.GameTest;
+import net.minecraft.test.TestContext;
+import net.minecraft.util.math.BlockPos;
+
+import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
+import net.fabricmc.fabric.test.item.CustomDamageTest;
+
+public class FurnaceGameTest implements FabricGameTest {
+	private static final int COOK_TIME = 200;
+	private static final BlockPos POS = new BlockPos(0, 1, 0);
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void basicSmelt(TestContext context) {
+		context.setBlockState(POS, Blocks.FURNACE);
+		FurnaceBlockEntity blockEntity = (FurnaceBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS));
+
+		setInputs(blockEntity, new ItemStack(Blocks.COBBLESTONE, 8), new ItemStack(Items.COAL, 2));
+
+		cook(blockEntity, context, 1);
+		assertInventory(blockEntity, "Testing vanilla smelting.",
+				new ItemStack(Blocks.COBBLESTONE, 7),
+				new ItemStack(Items.COAL, 1),
+				new ItemStack(Blocks.STONE, 1));
+
+		cook(blockEntity, context, 7);
+		assertInventory(blockEntity, "Testing vanilla smelting.",
+				ItemStack.EMPTY,
+				new ItemStack(Items.COAL, 1),
+				new ItemStack(Blocks.STONE, 8));
+
+		context.complete();
+	}
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void vanillaRemainderTest(TestContext context) {
+		context.setBlockState(POS, Blocks.FURNACE);
+		FurnaceBlockEntity blockEntity = (FurnaceBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS));
+
+		setInputs(blockEntity, new ItemStack(Blocks.COBBLESTONE, 64), new ItemStack(Items.LAVA_BUCKET));
+
+		cook(blockEntity, context, 64);
+		assertInventory(blockEntity, "Testing vanilla smelting recipe remainder.",
+				ItemStack.EMPTY,
+				new ItemStack(Items.BUCKET),
+				new ItemStack(Blocks.STONE, 64));
+
+		context.complete();
+	}
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void fabricRemainderTest(TestContext context) {
+		context.setBlockState(POS, Blocks.FURNACE);
+		FurnaceBlockEntity blockEntity = (FurnaceBlockEntity) Objects.requireNonNull(context.getBlockEntity(POS));
+
+		setInputs(blockEntity, new ItemStack(Blocks.COBBLESTONE, 32), new ItemStack(CustomDamageTest.WEIRD_PICK));
+
+		cook(blockEntity, context, 1);
+		assertInventory(blockEntity, "Testing fabric smelting recipe remainder.",
+				new ItemStack(Blocks.COBBLESTONE, 31),
+				RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 1),
+				new ItemStack(Blocks.STONE, 1));
+
+		cook(blockEntity, context, 30);
+		assertInventory(blockEntity, "Testing fabric smelting recipe remainder.",
+				new ItemStack(Blocks.COBBLESTONE, 1),
+				RecipeGameTest.withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 31),
+				new ItemStack(Blocks.STONE, 31));
+
+		cook(blockEntity, context, 1);
+		assertInventory(blockEntity, "Testing fabric smelting recipe remainder.",
+				ItemStack.EMPTY,
+				ItemStack.EMPTY,
+				new ItemStack(Blocks.STONE, 32));
+
+		context.complete();
+	}
+
+	private void setInputs(FurnaceBlockEntity blockEntity, ItemStack ingredient, ItemStack fuel) {
+		blockEntity.setStack(0, ingredient);
+		blockEntity.setStack(1, fuel);
+	}
+
+	private void assertInventory(FurnaceBlockEntity blockEntity, String extraErrorInfo, ItemStack... stacks) {
+		for (int i = 0; i < stacks.length; i++) {
+			ItemStack currentStack = blockEntity.getStack(i);
+			ItemStack expectedStack = stacks[i];
+
+			RecipeGameTest.assertStacks(currentStack, expectedStack, extraErrorInfo);
+		}
+	}
+
+	private void cook(FurnaceBlockEntity blockEntity, TestContext context, int items) {
+		for (int i = 0; i < COOK_TIME * items; i++) {
+			AbstractFurnaceBlockEntity.tick(context.getWorld(), POS, context.getBlockState(POS), blockEntity);
+		}
+	}
+}
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java
new file mode 100644
index 000000000..53b03e9ea
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/RecipeGameTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.item.gametest;
+
+import net.minecraft.inventory.SimpleInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.recipe.Recipe;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.RecipeType;
+import net.minecraft.test.GameTest;
+import net.minecraft.test.GameTestException;
+import net.minecraft.test.TestContext;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.collection.DefaultedList;
+import net.minecraft.world.World;
+
+import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
+import net.fabricmc.fabric.test.item.CustomDamageTest;
+
+public class RecipeGameTest implements FabricGameTest {
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void vanillaRemainderTest(TestContext context) {
+		Recipe<SimpleInventory> testRecipe = createTestingRecipeInstance();
+
+		SimpleInventory inventory = new SimpleInventory(
+				new ItemStack(Items.WATER_BUCKET),
+				new ItemStack(Items.DIAMOND));
+
+		DefaultedList<ItemStack> remainderList = testRecipe.getRemainder(inventory);
+
+		assertStackList(remainderList, "Testing vanilla recipe remainder.",
+				new ItemStack(Items.BUCKET),
+				ItemStack.EMPTY);
+
+		context.complete();
+	}
+
+	@GameTest(structureName = EMPTY_STRUCTURE)
+	public void fabricRemainderTest(TestContext context) {
+		Recipe<SimpleInventory> testRecipe = createTestingRecipeInstance();
+
+		SimpleInventory inventory = new SimpleInventory(
+				new ItemStack(CustomDamageTest.WEIRD_PICK),
+				withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 10),
+				withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 31),
+				new ItemStack(Items.DIAMOND));
+
+		DefaultedList<ItemStack> remainderList = testRecipe.getRemainder(inventory);
+
+		assertStackList(remainderList, "Testing fabric recipe remainder.",
+				withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 1),
+				withDamage(new ItemStack(CustomDamageTest.WEIRD_PICK), 11),
+				ItemStack.EMPTY,
+				ItemStack.EMPTY);
+
+		context.complete();
+	}
+
+	private Recipe<SimpleInventory> createTestingRecipeInstance() {
+		return new Recipe<>() {
+			@Override
+			public boolean matches(SimpleInventory inventory, World world) {
+				return true;
+			}
+
+			@Override
+			public ItemStack craft(SimpleInventory inventory) {
+				return null;
+			}
+
+			@Override
+			public boolean fits(int width, int height) {
+				return true;
+			}
+
+			@Override
+			public ItemStack getOutput() {
+				return null;
+			}
+
+			@Override
+			public Identifier getId() {
+				return null;
+			}
+
+			@Override
+			public RecipeSerializer<?> getSerializer() {
+				return null;
+			}
+
+			@Override
+			public RecipeType<?> getType() {
+				return null;
+			}
+		};
+	}
+
+	private void assertStackList(DefaultedList<ItemStack> stackList, String extraErrorInfo, ItemStack... stacks) {
+		for (int i = 0; i < stackList.size(); i++) {
+			ItemStack currentStack = stackList.get(i);
+			ItemStack expectedStack = stacks[i];
+
+			assertStacks(currentStack, expectedStack, extraErrorInfo);
+		}
+	}
+
+	static void assertStacks(ItemStack currentStack, ItemStack expectedStack, String extraErrorInfo) {
+		if (currentStack.isEmpty() && expectedStack.isEmpty()) {
+			return;
+		}
+
+		if (!currentStack.isItemEqual(expectedStack)) {
+			throw new GameTestException("Item stacks dont match. " + extraErrorInfo);
+		}
+
+		if (currentStack.getCount() != expectedStack.getCount()) {
+			throw new GameTestException("Size doesnt match. " + extraErrorInfo);
+		}
+
+		if (!ItemStack.areNbtEqual(currentStack, expectedStack)) {
+			throw new GameTestException("Nbt doesnt match. " + extraErrorInfo);
+		}
+	}
+
+	static ItemStack withDamage(ItemStack stack, int damage) {
+		stack.setDamage(damage);
+		return stack;
+	}
+}
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.java
new file mode 100644
index 000000000..2bdaffced
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/mixin/BrewingRecipeRegistryAccessor.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.test.item.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+import net.minecraft.item.Item;
+import net.minecraft.potion.Potion;
+import net.minecraft.recipe.BrewingRecipeRegistry;
+
+@Mixin(BrewingRecipeRegistry.class)
+public interface BrewingRecipeRegistryAccessor {
+	@Invoker
+	static void callRegisterPotionRecipe(Potion input, Item item, Potion output) {
+		throw new UnsupportedOperationException();
+	}
+}
diff --git a/fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json b/fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json
new file mode 100644
index 000000000..c89f4aa3b
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/resources/data/fabric-item-api-v1-testmod/recipes/weird_pickaxe.json
@@ -0,0 +1,15 @@
+{
+  "type": "minecraft:crafting_shapeless",
+  "ingredients": [
+    {
+      "item": "minecraft:diamond_ore"
+    },
+    {
+      "item": "fabric-item-api-v1-testmod:weird_pickaxe"
+    }
+  ],
+  "result": {
+    "item": "minecraft:diamond",
+    "count": 1
+  }
+}
diff --git a/fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json b/fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json
new file mode 100644
index 000000000..6a2e72344
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/resources/fabric-item-api-tests-v1.mixins.json
@@ -0,0 +1,11 @@
+{
+  "required": true,
+  "package": "net.fabricmc.fabric.test.item.mixin",
+  "compatibilityLevel": "JAVA_16",
+  "mixins": [
+    "BrewingRecipeRegistryAccessor"
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/fabric-item-api-v1/src/testmod/resources/fabric.mod.json b/fabric-item-api-v1/src/testmod/resources/fabric.mod.json
index 93e8e9873..56e2f60a6 100644
--- a/fabric-item-api-v1/src/testmod/resources/fabric.mod.json
+++ b/fabric-item-api-v1/src/testmod/resources/fabric.mod.json
@@ -18,6 +18,14 @@
     ],
     "client": [
       "net.fabricmc.fabric.test.item.client.TooltipTests"
+    ],
+    "fabric-gametest" : [
+      "net.fabricmc.fabric.test.item.gametest.BrewingStandGameTest",
+      "net.fabricmc.fabric.test.item.gametest.FurnaceGameTest",
+      "net.fabricmc.fabric.test.item.gametest.RecipeGameTest"
     ]
-  }
+  },
+  "mixins": [
+    "fabric-item-api-tests-v1.mixins.json"
+  ]
 }