From d38542c645cb256abe22029bfece6cc43b05f1ec Mon Sep 17 00:00:00 2001
From: haykam821 <24855774+haykam821@users.noreply.github.com>
Date: Mon, 23 Sep 2024 13:49:11 -0400
Subject: [PATCH] Add fuel registry events (#4038)

* Add fuel registry events

* Allow removing individual items from fuel registry builders

* Expand fuel registry events test coverage

* Fix incorrect fuel registry documentation

* Review changes

* Use a context object

* Checkstyle

---------

Co-authored-by: modmuss50 <modmuss50@gmail.com>
---
 .../api/registry/FuelRegistryEvents.java      | 99 +++++++++++++++++++
 .../FuelRegistryEventsContextImpl.java        | 25 +++++
 .../content/registry/FuelRegistryMixin.java   | 62 ++++++++++++
 .../fabric-content-registries-v0.mixins.json  |  1 +
 .../registry/ContentRegistryGameTest.java     | 87 ++++++++++++++++
 .../content/registry/ContentRegistryTest.java | 41 +++++++-
 .../lang/en_us.json                           |  9 ++
 .../item/smelting_fuels_excluded_by_tag.json  |  5 +
 .../item/smelting_fuels_included_by_tag.json  |  7 ++
 .../tags/item/non_flammable_wood.json         |  5 +
 .../fabric/test/item/CustomDamageTest.java    |  2 +
 .../test/item/gametest/FurnaceGameTest.java   |  3 +-
 12 files changed, 340 insertions(+), 6 deletions(-)
 create mode 100644 fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/api/registry/FuelRegistryEvents.java
 create mode 100644 fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/impl/content/registry/FuelRegistryEventsContextImpl.java
 create mode 100644 fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/mixin/content/registry/FuelRegistryMixin.java
 create mode 100644 fabric-content-registries-v0/src/testmod/resources/assets/fabric-content-registries-v0-testmod/lang/en_us.json
 create mode 100644 fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_excluded_by_tag.json
 create mode 100644 fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_included_by_tag.json
 create mode 100644 fabric-content-registries-v0/src/testmod/resources/data/minecraft/tags/item/non_flammable_wood.json

diff --git a/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/api/registry/FuelRegistryEvents.java b/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/api/registry/FuelRegistryEvents.java
new file mode 100644
index 000000000..2116f8dee
--- /dev/null
+++ b/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/api/registry/FuelRegistryEvents.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.registry;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.item.FuelRegistry;
+import net.minecraft.registry.RegistryWrapper;
+import net.minecraft.resource.featuretoggle.FeatureSet;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+/**
+ * Contains events to aid in modifying fuels.
+ */
+@ApiStatus.NonExtendable
+public interface FuelRegistryEvents {
+	/**
+	 * An event that is called when the fuel registry is being built after vanilla fuels have been registered and before exclusions have been applied.
+	 */
+	Event<FuelRegistryEvents.BuildCallback> BUILD = EventFactory.createArrayBacked(FuelRegistryEvents.BuildCallback.class, listeners -> (builder, context) -> {
+		for (FuelRegistryEvents.BuildCallback listener : listeners) {
+			listener.build(builder, context);
+		}
+	});
+
+	/**
+	 * An event that is called when the fuel registry is being built after vanilla exclusions have been applied.
+	 */
+	Event<FuelRegistryEvents.ExclusionsCallback> EXCLUSIONS = EventFactory.createArrayBacked(FuelRegistryEvents.ExclusionsCallback.class, listeners -> (builder, context) -> {
+		for (FuelRegistryEvents.ExclusionsCallback listener : listeners) {
+			listener.buildExclusions(builder, context);
+		}
+	});
+
+	@ApiStatus.NonExtendable
+	interface Context {
+		/**
+		 * Get the base smelt time for the fuel, for furnaces this defaults to 200.
+		 * @return the base smelt time
+		 */
+		int baseSmeltTime();
+
+		/**
+		 * Get the {@link RegistryWrapper.WrapperLookup} for all registries.
+		 * @return the registry lookup
+		 */
+		RegistryWrapper.WrapperLookup registries();
+
+		/**
+		 * Get the currently enabled feature set.
+		 * @return the {@link FeatureSet} instance
+		 */
+		FeatureSet enabledFeatures();
+	}
+
+	/**
+	 * Use this event to register custom fuels.
+	 */
+	@FunctionalInterface
+	interface BuildCallback {
+		/**
+		 * Called when the fuel registry is being built after vanilla fuels have been registered and before exclusions have been applied.
+		 *
+		 * @param builder the builder being used to construct a {@link FuelRegistry} instance
+		 * @param context the context for the event
+		 */
+		void build(FuelRegistry.Builder builder, Context context);
+	}
+
+	/**
+	 * Use this event to register custom fuels.
+	 */
+	@FunctionalInterface
+	interface ExclusionsCallback {
+		/**
+		 * Called when the fuel registry is being built after vanilla exclusions have been applied.
+		 *
+		 * @param builder the builder being used to construct a {@link FuelRegistry} instance
+		 * @param context the context for the event
+		 */
+		void buildExclusions(FuelRegistry.Builder builder, Context context);
+	}
+}
diff --git a/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/impl/content/registry/FuelRegistryEventsContextImpl.java b/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/impl/content/registry/FuelRegistryEventsContextImpl.java
new file mode 100644
index 000000000..da585280f
--- /dev/null
+++ b/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/impl/content/registry/FuelRegistryEventsContextImpl.java
@@ -0,0 +1,25 @@
+/*
+ * 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.content.registry;
+
+import net.minecraft.registry.RegistryWrapper;
+import net.minecraft.resource.featuretoggle.FeatureSet;
+
+import net.fabricmc.fabric.api.registry.FuelRegistryEvents;
+
+public record FuelRegistryEventsContextImpl(RegistryWrapper.WrapperLookup registries, FeatureSet enabledFeatures, int baseSmeltTime) implements FuelRegistryEvents.Context {
+}
diff --git a/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/mixin/content/registry/FuelRegistryMixin.java b/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/mixin/content/registry/FuelRegistryMixin.java
new file mode 100644
index 000000000..2af067d5c
--- /dev/null
+++ b/fabric-content-registries-v0/src/main/java/net/fabricmc/fabric/mixin/content/registry/FuelRegistryMixin.java
@@ -0,0 +1,62 @@
+/*
+ * 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.content.registry;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import net.minecraft.item.FuelRegistry;
+import net.minecraft.item.Item;
+import net.minecraft.registry.RegistryWrapper;
+import net.minecraft.registry.tag.TagKey;
+import net.minecraft.resource.featuretoggle.FeatureSet;
+
+import net.fabricmc.fabric.api.registry.FuelRegistryEvents;
+import net.fabricmc.fabric.impl.content.registry.FuelRegistryEventsContextImpl;
+
+/**
+ * Implements the invocation of {@link FabricFuelRegistryBuilder} callbacks.
+ */
+@Mixin(FuelRegistry.class)
+public abstract class FuelRegistryMixin {
+	/**
+	 * Handles invoking both pre- and post-exclusion events.
+	 *
+	 * <p>Vanilla currently uses a single exclusion for non-flammable wood; if more builder calls for exclusions are added, this mixin method must be split accordingly.
+	 */
+	@WrapOperation(
+			method = "createDefault(Lnet/minecraft/registry/RegistryWrapper$WrapperLookup;Lnet/minecraft/resource/featuretoggle/FeatureSet;I)Lnet/minecraft/item/FuelRegistry;",
+			at = @At(
+					value = "INVOKE",
+					target = "Lnet/minecraft/item/FuelRegistry$Builder;remove(Lnet/minecraft/registry/tag/TagKey;)Lnet/minecraft/item/FuelRegistry$Builder;"
+			),
+			allow = 1
+	)
+	private static FuelRegistry.Builder build(FuelRegistry.Builder builder, TagKey<Item> tag, Operation<FuelRegistry.Builder> operation, @Local(argsOnly = true) RegistryWrapper.WrapperLookup registries, @Local(argsOnly = true) FeatureSet features, @Local(argsOnly = true) int baseSmeltTime) {
+		final var context = new FuelRegistryEventsContextImpl(registries, features, baseSmeltTime);
+
+		FuelRegistryEvents.BUILD.invoker().build(builder, context);
+
+		operation.call(builder, tag);
+		FuelRegistryEvents.EXCLUSIONS.invoker().buildExclusions(builder, context);
+
+		return builder;
+	}
+}
diff --git a/fabric-content-registries-v0/src/main/resources/fabric-content-registries-v0.mixins.json b/fabric-content-registries-v0/src/main/resources/fabric-content-registries-v0.mixins.json
index c541562b1..453015f14 100644
--- a/fabric-content-registries-v0/src/main/resources/fabric-content-registries-v0.mixins.json
+++ b/fabric-content-registries-v0/src/main/resources/fabric-content-registries-v0.mixins.json
@@ -7,6 +7,7 @@
     "BrewingRecipeRegistryBuilderMixin",
     "PathContextMixin",
     "FarmerWorkTaskAccessor",
+    "FuelRegistryMixin",
     "GiveGiftsToHeroTaskAccessor",
     "GiveGiftsToHeroTaskMixin",
     "HoeItemAccessor",
diff --git a/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryGameTest.java b/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryGameTest.java
index 588008e41..65132ecfc 100644
--- a/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryGameTest.java
+++ b/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryGameTest.java
@@ -16,11 +16,16 @@
 
 package net.fabricmc.fabric.test.content.registry;
 
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
+import net.minecraft.block.BlockState;
 import net.minecraft.block.Blocks;
 import net.minecraft.block.ComposterBlock;
+import net.minecraft.block.HopperBlock;
+import net.minecraft.block.entity.AbstractFurnaceBlockEntity;
 import net.minecraft.block.entity.BrewingStandBlockEntity;
+import net.minecraft.block.entity.HopperBlockEntity;
 import net.minecraft.component.DataComponentTypes;
 import net.minecraft.component.type.PotionContentsComponent;
 import net.minecraft.entity.player.PlayerEntity;
@@ -31,6 +36,7 @@ import net.minecraft.test.GameTest;
 import net.minecraft.test.TestContext;
 import net.minecraft.util.Hand;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.GameMode;
 
 import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
@@ -63,6 +69,87 @@ public class ContentRegistryGameTest {
 		context.complete();
 	}
 
+	private void smelt(TestContext context, ItemStack fuelStack, BiConsumer<AbstractFurnaceBlockEntity, HopperBlockEntity> callback) {
+		// Create a furnace to simulate smelting in
+		// A blast furnace will smelt twice as fast, so it is used here
+		var furnacePos = new BlockPos(0, 1, 0);
+		BlockState furnaceState = Blocks.BLAST_FURNACE.getDefaultState();
+
+		context.setBlockState(furnacePos, furnaceState);
+
+		if (!(context.getBlockEntity(furnacePos) instanceof AbstractFurnaceBlockEntity furnace)) {
+			throw new AssertionError("Furnace was not placed");
+		}
+
+		// Create a hopper that attempts to insert fuel into the furnace
+		BlockPos hopperPos = furnacePos.east();
+		BlockState hopperState = Blocks.HOPPER.getDefaultState()
+				.with(HopperBlock.FACING, context.getRotation().rotate(Direction.WEST));
+
+		context.setBlockState(hopperPos, hopperState);
+
+		if (!(context.getBlockEntity(hopperPos) instanceof HopperBlockEntity hopper)) {
+			throw new AssertionError("Hopper was not placed");
+		}
+
+		// Insert the fuel into the hopper, which transfers it into the furnace
+		hopper.setStack(0, fuelStack.copy());
+
+		// Insert the item that should be smelted into the furnace
+		// Smelting a single item takes 200 fuel time
+		furnace.setStack(0, new ItemStack(Items.RAW_IRON, 1));
+
+		context.waitAndRun(105, () -> callback.accept(furnace, hopper));
+	}
+
+	private void smeltCompleted(TestContext context, ItemStack fuelStack) {
+		smelt(context, fuelStack, (furnace, hopper) -> {
+			context.assertTrue(hopper.isEmpty(), "fuel hopper should have been emptied");
+
+			context.assertTrue(furnace.getStack(0).isEmpty(), "furnace input slot should have been emptied");
+			context.assertTrue(furnace.getStack(0).isEmpty(), "furnace fuel slot should have been emptied");
+			context.assertTrue(ItemStack.areEqual(furnace.getStack(2), new ItemStack(Items.IRON_INGOT, 1)), "one iron ingot should have been smelted and placed into the furnace output slot");
+
+			context.complete();
+		});
+	}
+
+	private void smeltFailed(TestContext context, ItemStack fuelStack) {
+		smelt(context, fuelStack, (furnace, hopper) -> {
+			context.assertTrue(ItemStack.areEqual(hopper.getStack(0), fuelStack), "fuel hopper should not have been emptied");
+
+			context.assertTrue(ItemStack.areEqual(furnace.getStack(0), new ItemStack(Items.RAW_IRON, 1)), "furnace input slot should not have been emptied");
+			context.assertTrue(furnace.getStack(1).isEmpty(), "furnace fuel slot should not have been filled");
+			context.assertTrue(furnace.getStack(2).isEmpty(), "furnace output slot should not have been filled");
+
+			context.complete();
+		});
+	}
+
+	@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE, tickLimit = 110)
+	public void testSmeltingFuelIncludedByItem(TestContext context) {
+		// Item with 50 fuel time x4 = 200 fuel time
+		smeltCompleted(context, new ItemStack(ContentRegistryTest.SMELTING_FUEL_INCLUDED_BY_ITEM, 4));
+	}
+
+	@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE, tickLimit = 110)
+	public void testSmeltingFuelIncludedByTag(TestContext context) {
+		// Item in tag with 100 fuel time x2 = 200 fuel time
+		smeltCompleted(context, new ItemStack(ContentRegistryTest.SMELTING_FUEL_INCLUDED_BY_TAG, 2));
+	}
+
+	@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE, tickLimit = 110)
+	public void testSmeltingFuelExcludedByTag(TestContext context) {
+		// Item is in both the smelting fuels tag and the excluded smithing fuels tag
+		smeltFailed(context, new ItemStack(ContentRegistryTest.SMELTING_FUEL_EXCLUDED_BY_TAG));
+	}
+
+	@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE, tickLimit = 110)
+	public void testSmeltingFuelExcludedByVanillaTag(TestContext context) {
+		// Item is in both the smelting fuel tag and vanilla's excluded non-flammable wood tag
+		smeltFailed(context, new ItemStack(ContentRegistryTest.SMELTING_FUEL_EXCLUDED_BY_VANILLA_TAG));
+	}
+
 	@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
 	public void testStrippableBlockRegistry(TestContext context) {
 		BlockPos pos = new BlockPos(0, 1, 0);
diff --git a/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryTest.java b/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryTest.java
index ca520bc74..b2429f595 100644
--- a/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryTest.java
+++ b/fabric-content-registries-v0/src/testmod/java/net/fabricmc/fabric/test/content/registry/ContentRegistryTest.java
@@ -39,6 +39,7 @@ import net.minecraft.registry.RegistryKeys;
 import net.minecraft.registry.entry.RegistryEntry;
 import net.minecraft.registry.tag.BlockTags;
 import net.minecraft.registry.tag.ItemTags;
+import net.minecraft.registry.tag.TagKey;
 import net.minecraft.resource.featuretoggle.FeatureFlags;
 import net.minecraft.text.Text;
 import net.minecraft.util.ActionResult;
@@ -54,6 +55,7 @@ import net.fabricmc.fabric.api.registry.CompostingChanceRegistry;
 import net.fabricmc.fabric.api.registry.FabricBrewingRecipeRegistryBuilder;
 import net.fabricmc.fabric.api.registry.FlammableBlockRegistry;
 import net.fabricmc.fabric.api.registry.FlattenableBlockRegistry;
+import net.fabricmc.fabric.api.registry.FuelRegistryEvents;
 import net.fabricmc.fabric.api.registry.LandPathNodeTypesRegistry;
 import net.fabricmc.fabric.api.registry.OxidizableBlocksRegistry;
 import net.fabricmc.fabric.api.registry.SculkSensorFrequencyRegistry;
@@ -62,9 +64,18 @@ import net.fabricmc.fabric.api.registry.TillableBlockRegistry;
 import net.fabricmc.fabric.api.registry.VillagerInteractionRegistries;
 
 public final class ContentRegistryTest implements ModInitializer {
+	public static final String MOD_ID = "fabric-content-registries-v0-testmod";
 	public static final Logger LOGGER = LoggerFactory.getLogger(ContentRegistryTest.class);
 
-	public static final Identifier TEST_EVENT_ID = Identifier.of("fabric-content-registries-v0-testmod", "test_event");
+	public static final Item SMELTING_FUEL_INCLUDED_BY_ITEM = registerItem("smelting_fuel_included_by_item");
+	public static final Item SMELTING_FUEL_INCLUDED_BY_TAG = registerItem("smelting_fuel_included_by_tag");
+	public static final Item SMELTING_FUEL_EXCLUDED_BY_TAG = registerItem("smelting_fuel_excluded_by_tag");
+	public static final Item SMELTING_FUEL_EXCLUDED_BY_VANILLA_TAG = registerItem("smelting_fuel_excluded_by_vanilla_tag");
+
+	private static final TagKey<Item> SMELTING_FUELS_INCLUDED_BY_TAG = itemTag("smelting_fuels_included_by_tag");
+	private static final TagKey<Item> SMELTING_FUELS_EXCLUDED_BY_TAG = itemTag("smelting_fuels_excluded_by_tag");
+
+	public static final Identifier TEST_EVENT_ID = id("test_event");
 	public static final RegistryKey<Block> TEST_EVENT_BLOCK_KEY = RegistryKey.of(RegistryKeys.BLOCK, TEST_EVENT_ID);
 	public static final RegistryEntry.Reference<GameEvent> TEST_EVENT = Registry.registerReference(Registries.GAME_EVENT, TEST_EVENT_ID, new GameEvent(GameEvent.DEFAULT_RANGE));
 
@@ -75,8 +86,7 @@ public final class ContentRegistryTest implements ModInitializer {
 		//  - diamond block is now flammable
 		//  - sand is now flammable
 		//  - red wool is flattenable to yellow wool
-		//  - obsidian is now fuel
-		//  - all items with the tag 'minecraft:dirt' are now fuel
+		//  - custom items prefixed with 'smelting fuels included by' are valid smelting fuels
 		//  - dead bush is now considered as a dangerous block like sweet berry bushes (all entities except foxes should avoid it)
 		//  - quartz pillars are strippable to hay blocks
 		//  - green wool is tillable to lime wool
@@ -94,6 +104,16 @@ public final class ContentRegistryTest implements ModInitializer {
 		FlammableBlockRegistry.getDefaultInstance().add(Blocks.DIAMOND_BLOCK, 4, 4);
 		FlammableBlockRegistry.getDefaultInstance().add(BlockTags.SAND, 4, 4);
 		FlattenableBlockRegistry.register(Blocks.RED_WOOL, Blocks.YELLOW_WOOL.getDefaultState());
+
+		FuelRegistryEvents.BUILD.register((builder, context) -> {
+			builder.add(SMELTING_FUEL_INCLUDED_BY_ITEM, context.baseSmeltTime() / 4);
+			builder.add(SMELTING_FUELS_INCLUDED_BY_TAG, context.baseSmeltTime() / 2);
+		});
+
+		FuelRegistryEvents.EXCLUSIONS.register((builder, context) -> {
+			builder.remove(SMELTING_FUELS_EXCLUDED_BY_TAG);
+		});
+
 		LandPathNodeTypesRegistry.register(Blocks.DEAD_BUSH, PathNodeType.DAMAGE_OTHER, PathNodeType.DANGER_OTHER);
 		StrippableBlockRegistry.register(Blocks.QUARTZ_PILLAR, Blocks.HAY_BLOCK);
 
@@ -150,7 +170,7 @@ public final class ContentRegistryTest implements ModInitializer {
 			LOGGER.info("SculkSensorFrequencyRegistry test passed!");
 		}
 
-		RegistryKey<Item> dirtyPotionKey = RegistryKey.of(RegistryKeys.ITEM, Identifier.of("fabric-content-registries-v0-testmod", "dirty_potion"));
+		RegistryKey<Item> dirtyPotionKey = RegistryKey.of(RegistryKeys.ITEM, id("dirty_potion"));
 		var dirtyPotion = new DirtyPotionItem(new Item.Settings().maxCount(1).registryKey(dirtyPotionKey));
 		Registry.register(Registries.ITEM, dirtyPotionKey, dirtyPotion);
 		/* Mods should use BrewingRecipeRegistry.registerPotionType(Item), which is access widened by fabric-transitive-access-wideners-v1
@@ -189,4 +209,17 @@ public final class ContentRegistryTest implements ModInitializer {
 			return Text.literal("Dirty ").append(Items.POTION.getName(stack));
 		}
 	}
+
+	private static Identifier id(String path) {
+		return Identifier.of(MOD_ID, path);
+	}
+
+	private static Item registerItem(String path) {
+		RegistryKey<Item> key = RegistryKey.of(RegistryKeys.ITEM, id(path));
+		return Registry.register(Registries.ITEM, key, new Item(new Item.Settings().registryKey(key)));
+	}
+
+	private static TagKey<Item> itemTag(String path) {
+		return TagKey.of(RegistryKeys.ITEM, id(path));
+	}
 }
diff --git a/fabric-content-registries-v0/src/testmod/resources/assets/fabric-content-registries-v0-testmod/lang/en_us.json b/fabric-content-registries-v0/src/testmod/resources/assets/fabric-content-registries-v0-testmod/lang/en_us.json
new file mode 100644
index 000000000..3fc809efa
--- /dev/null
+++ b/fabric-content-registries-v0/src/testmod/resources/assets/fabric-content-registries-v0-testmod/lang/en_us.json
@@ -0,0 +1,9 @@
+{
+  "item.fabric-content-registries-v0-testmod.smelting_fuel_excluded_by_item": "Smelting Fuel Excluded by Item",
+  "item.fabric-content-registries-v0-testmod.smelting_fuel_excluded_by_tag": "Smelting Fuel Excluded by Tag",
+  "item.fabric-content-registries-v0-testmod.smelting_fuel_excluded_by_vanilla_tag": "Smelting Fuel Excluded by Vanilla Tag",
+  "item.fabric-content-registries-v0-testmod.smelting_fuel_included_by_item": "Smelting Fuel Included by Item",
+  "item.fabric-content-registries-v0-testmod.smelting_fuel_included_by_tag": "Smelting Fuel Included by Tag",
+  "tag.item.fabric-content-registries-v0-testmod.smelting_fuels_excluded_by_tag": "Smelting Fuels Excluded by Tag",
+  "tag.item.fabric-content-registries-v0-testmod.smelting_fuels_included_by_tag": "Smelting Fuels Included by Tag"
+}
diff --git a/fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_excluded_by_tag.json b/fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_excluded_by_tag.json
new file mode 100644
index 000000000..d14ad7b9a
--- /dev/null
+++ b/fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_excluded_by_tag.json
@@ -0,0 +1,5 @@
+{
+  "values": [
+    "fabric-content-registries-v0-testmod:smelting_fuel_excluded_by_tag"
+  ]
+}
diff --git a/fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_included_by_tag.json b/fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_included_by_tag.json
new file mode 100644
index 000000000..be4991af1
--- /dev/null
+++ b/fabric-content-registries-v0/src/testmod/resources/data/fabric-content-registries-v0-testmod/tags/item/smelting_fuels_included_by_tag.json
@@ -0,0 +1,7 @@
+{
+  "values": [
+    "fabric-content-registries-v0-testmod:smelting_fuel_included_by_tag",
+    "fabric-content-registries-v0-testmod:smelting_fuel_excluded_by_tag",
+    "fabric-content-registries-v0-testmod:smelting_fuel_excluded_by_vanilla_tag"
+  ]
+}
diff --git a/fabric-content-registries-v0/src/testmod/resources/data/minecraft/tags/item/non_flammable_wood.json b/fabric-content-registries-v0/src/testmod/resources/data/minecraft/tags/item/non_flammable_wood.json
new file mode 100644
index 000000000..a9243ffb4
--- /dev/null
+++ b/fabric-content-registries-v0/src/testmod/resources/data/minecraft/tags/item/non_flammable_wood.json
@@ -0,0 +1,5 @@
+{
+  "values": [
+    "fabric-content-registries-v0-testmod:smelting_fuel_excluded_by_vanilla_tag"
+  ]
+}
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 58f14cd05..c01f8e346 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
@@ -42,6 +42,7 @@ import net.fabricmc.fabric.api.item.v1.CustomDamageHandler;
 import net.fabricmc.fabric.api.item.v1.EnchantingContext;
 import net.fabricmc.fabric.api.item.v1.EnchantmentEvents;
 import net.fabricmc.fabric.api.registry.FabricBrewingRecipeRegistryBuilder;
+import net.fabricmc.fabric.api.registry.FuelRegistryEvents;
 import net.fabricmc.fabric.api.util.TriState;
 
 public class CustomDamageTest implements ModInitializer {
@@ -63,6 +64,7 @@ public class CustomDamageTest implements ModInitializer {
 	@Override
 	public void onInitialize() {
 		Registry.register(Registries.ITEM, WEIRD_PICK_KEY, WEIRD_PICK);
+		FuelRegistryEvents.BUILD.register((builder, context) -> builder.add(WEIRD_PICK, context.baseSmeltTime()));
 		FabricBrewingRecipeRegistryBuilder.BUILD.register(builder -> builder.registerPotionRecipe(Potions.WATER, WEIRD_PICK, Potions.AWKWARD));
 		EnchantmentEvents.ALLOW_ENCHANTING.register(((enchantment, target, enchantingContext) -> {
 			if (target.isOf(Items.DIAMOND_PICKAXE) && enchantment.matchesKey(Enchantments.SHARPNESS) && EnchantmentHelper.hasAnyEnchantmentsIn(target, EnchantmentTags.MINING_EXCLUSIVE_SET)) {
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
index 04528abb1..b5933aa39 100644
--- 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
@@ -70,8 +70,7 @@ public class FurnaceGameTest implements FabricGameTest {
 		context.complete();
 	}
 
-	// TODO 1.21.2 - test broken as the weird pick does not have a fuel value, so doesnt burn.
-	// @GameTest(templateName = EMPTY_STRUCTURE)
+	@GameTest(templateName = EMPTY_STRUCTURE)
 	public void fabricRemainderTest(TestContext context) {
 		context.setBlockState(POS, Blocks.FURNACE);
 		FurnaceBlockEntity blockEntity = context.getBlockEntity(POS);