From 5c754785217512de3302f209e890712b503593a5 Mon Sep 17 00:00:00 2001
From: modmuss <modmuss50@gmail.com>
Date: Tue, 7 May 2024 19:03:43 +0100
Subject: [PATCH] Add API to modify default item components (#3728)

* Add API to modify default item components

* Add test for removal

* Some review feedback

* API design changes

* Review feedback

* Add overload that takes a Collection<Item>

(cherry picked from commit 5bcea88aba949b88a2135477d9c841bd99724658)
---
 .../item/v1/DefaultItemComponentEvents.java   | 84 +++++++++++++++++++
 .../impl/item/DefaultItemComponentImpl.java   | 51 +++++++++++
 .../fabric/mixin/item/ItemAccessor.java       | 31 +++++++
 .../fabric/mixin/item/RegistriesMixin.java    | 34 ++++++++
 .../resources/fabric-item-api-v1.mixins.json  |  4 +-
 .../test/item/DefaultItemComponentTest.java   | 63 ++++++++++++++
 .../DefaultItemComponentGameTest.java         | 76 +++++++++++++++++
 .../src/testmod/resources/fabric.mod.json     |  2 +
 8 files changed, 344 insertions(+), 1 deletion(-)
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/DefaultItemComponentEvents.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/DefaultItemComponentImpl.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemAccessor.java
 create mode 100644 fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RegistriesMixin.java
 create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/DefaultItemComponentTest.java
 create mode 100644 fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/DefaultItemComponentGameTest.java

diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/DefaultItemComponentEvents.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/DefaultItemComponentEvents.java
new file mode 100644
index 000000000..f1cd536dc
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/api/item/v1/DefaultItemComponentEvents.java
@@ -0,0 +1,84 @@
+/*
+ * 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 java.util.Collection;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import net.minecraft.component.ComponentMap;
+import net.minecraft.item.Item;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+/**
+ * Events to modify the default {@link ComponentMap} of items.
+ */
+public final class DefaultItemComponentEvents {
+	/**
+	 * Event used to add or remove data components to known items.
+	 */
+	public static final Event<ModifyCallback> MODIFY = EventFactory.createArrayBacked(ModifyCallback.class, listeners -> context -> {
+		for (ModifyCallback listener : listeners) {
+			listener.modify(context);
+		}
+	});
+
+	private DefaultItemComponentEvents() {
+	}
+
+	public interface ModifyContext {
+		/**
+		 * Modify the default data components of the specified item.
+		 *
+		 * @param itemPredicate A predicate to match items to modify
+		 * @param builderConsumer A consumer that provides a {@link ComponentMap.Builder} to modify the item's components.
+		 */
+		void modify(Predicate<Item> itemPredicate, BiConsumer<ComponentMap.Builder, Item> builderConsumer);
+
+		/**
+		 * Modify the default data components of the specified item.
+		 *
+		 * @param item The item to modify
+		 * @param builderConsumer A consumer that provides a {@link ComponentMap.Builder} to modify the item's components.
+		 */
+		default void modify(Item item, Consumer<ComponentMap.Builder> builderConsumer) {
+			modify(Predicate.isEqual(item), (builder, _item) -> builderConsumer.accept(builder));
+		}
+
+		/**
+		 * Modify the default data components of the specified items.
+		 * @param items The items to modify
+		 * @param builderConsumer A consumer that provides a {@link ComponentMap.Builder} to modify the item's components.
+		 */
+		default void modify(Collection<Item> items, BiConsumer<ComponentMap.Builder, Item> builderConsumer) {
+			modify(items::contains, builderConsumer);
+		}
+	}
+
+	@FunctionalInterface
+	public interface ModifyCallback {
+		/**
+		 * Modify the default data components of items using the provided {@link ModifyContext} instance.
+		 *
+		 * @param context The context to modify items
+		 */
+		void modify(ModifyContext context);
+	}
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/DefaultItemComponentImpl.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/DefaultItemComponentImpl.java
new file mode 100644
index 000000000..4091e56d2
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/impl/item/DefaultItemComponentImpl.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.util.function.BiConsumer;
+import java.util.function.Predicate;
+
+import net.minecraft.component.ComponentMap;
+import net.minecraft.item.Item;
+import net.minecraft.registry.Registries;
+
+import net.fabricmc.fabric.api.item.v1.DefaultItemComponentEvents;
+import net.fabricmc.fabric.mixin.item.ItemAccessor;
+
+public class DefaultItemComponentImpl {
+	public static void modifyItemComponents() {
+		DefaultItemComponentEvents.MODIFY.invoker().modify(ModifyContextImpl.INSTANCE);
+	}
+
+	static class ModifyContextImpl implements DefaultItemComponentEvents.ModifyContext {
+		private static final ModifyContextImpl INSTANCE = new ModifyContextImpl();
+
+		private ModifyContextImpl() {
+		}
+
+		@Override
+		public void modify(Predicate<Item> itemPredicate, BiConsumer<ComponentMap.Builder, Item> builderConsumer) {
+			for (Item item : Registries.ITEM) {
+				if (itemPredicate.test(item)) {
+					ComponentMap.Builder builder = ComponentMap.builder().addAll(item.getComponents());
+					builderConsumer.accept(builder, item);
+					((ItemAccessor) item).setComponents(builder.build());
+				}
+			}
+		}
+	}
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemAccessor.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemAccessor.java
new file mode 100644
index 000000000..69db4ef77
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/ItemAccessor.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.item;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.component.ComponentMap;
+import net.minecraft.item.Item;
+
+@Mixin(Item.class)
+public interface ItemAccessor {
+	@Accessor
+	@Mutable
+	void setComponents(ComponentMap components);
+}
diff --git a/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RegistriesMixin.java b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RegistriesMixin.java
new file mode 100644
index 000000000..19c3a5b85
--- /dev/null
+++ b/fabric-item-api-v1/src/main/java/net/fabricmc/fabric/mixin/item/RegistriesMixin.java
@@ -0,0 +1,34 @@
+/*
+ * 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.callback.CallbackInfo;
+
+import net.minecraft.registry.Registries;
+
+import net.fabricmc.fabric.impl.item.DefaultItemComponentImpl;
+
+@Mixin(Registries.class)
+public abstract class RegistriesMixin {
+	@Inject(method = "freezeRegistries", at = @At("HEAD"))
+	private static void modifyDefaultItemComponents(CallbackInfo ci) {
+		DefaultItemComponentImpl.modifyItemComponents();
+	}
+}
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 0a0e6e667..845c30509 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
@@ -9,11 +9,13 @@
     "EnchantCommandMixin",
     "EnchantmentHelperMixin",
     "EnchantRandomlyLootFunctionMixin",
+    "ItemAccessor",
     "ItemMixin",
     "ItemSettingsMixin",
     "ItemStackMixin",
     "LivingEntityMixin",
-    "RecipeMixin"
+    "RecipeMixin",
+    "RegistriesMixin"
   ],
   "injectors": {
     "defaultRequire": 1
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/DefaultItemComponentTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/DefaultItemComponentTest.java
new file mode 100644
index 000000000..230ebfc28
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/DefaultItemComponentTest.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.test.item;
+
+import java.util.List;
+
+import it.unimi.dsi.fastutil.ints.IntList;
+
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.component.type.FireworkExplosionComponent;
+import net.minecraft.component.type.FireworksComponent;
+import net.minecraft.item.Items;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.item.v1.DefaultItemComponentEvents;
+
+public class DefaultItemComponentTest implements ModInitializer {
+	@Override
+	public void onInitialize() {
+		Identifier latePhase = new Identifier("fabric-item-api-v1-testmod", "late");
+		DefaultItemComponentEvents.MODIFY.addPhaseOrdering(Event.DEFAULT_PHASE, latePhase);
+
+		DefaultItemComponentEvents.MODIFY.register(context -> {
+			context.modify(Items.GOLD_INGOT, builder -> {
+				builder.add(DataComponentTypes.ITEM_NAME, Text.literal("Fool's Gold").formatted(Formatting.GOLD));
+			});
+			context.modify(Items.GOLD_NUGGET, builder -> {
+				builder.add(DataComponentTypes.FIREWORKS, new FireworksComponent(1, List.of(
+					new FireworkExplosionComponent(FireworkExplosionComponent.Type.STAR, IntList.of(0x32a852), IntList.of(0x32a852), true, true)
+				)));
+			});
+			context.modify(Items.BEEF, builder -> {
+				// Remove the food component from beef
+				builder.add(DataComponentTypes.FOOD, null);
+			});
+		});
+
+		// Make all fireworks glint
+		DefaultItemComponentEvents.MODIFY.register(latePhase, context -> {
+			context.modify(item -> item.getComponents().contains(DataComponentTypes.FIREWORKS), (builder, item) -> {
+				builder.add(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true);
+			});
+		});
+	}
+}
diff --git a/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/DefaultItemComponentGameTest.java b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/DefaultItemComponentGameTest.java
new file mode 100644
index 000000000..f6856bad0
--- /dev/null
+++ b/fabric-item-api-v1/src/testmod/java/net/fabricmc/fabric/test/item/gametest/DefaultItemComponentGameTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.function.Consumer;
+
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.component.type.FireworksComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.test.GameTest;
+import net.minecraft.test.GameTestException;
+import net.minecraft.test.TestContext;
+import net.minecraft.text.Text;
+
+import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
+
+public class DefaultItemComponentGameTest implements FabricGameTest {
+	@GameTest(templateName = EMPTY_STRUCTURE)
+	public void modify(TestContext context) {
+		Consumer<Text> checkText = text -> {
+			if (text == null) {
+				throw new GameTestException("Item name component not found on gold ingot");
+			}
+
+			if (!"Fool's Gold".equals(text.getString())) {
+				throw new GameTestException("Item name component on gold ingot is not set");
+			}
+		};
+
+		Text text = Items.GOLD_INGOT.getComponents().get(DataComponentTypes.ITEM_NAME);
+		checkText.accept(text);
+
+		text = new ItemStack(Items.GOLD_INGOT).getComponents().get(DataComponentTypes.ITEM_NAME);
+		checkText.accept(text);
+
+		boolean isBeefFood = Items.BEEF.getComponents().contains(DataComponentTypes.FOOD);
+
+		if (isBeefFood) {
+			throw new GameTestException("Food component not removed from beef");
+		}
+
+		context.complete();
+	}
+
+	@GameTest(templateName = EMPTY_STRUCTURE)
+	public void afterModify(TestContext context) {
+		FireworksComponent fireworksComponent = Items.GOLD_NUGGET.getComponents().get(DataComponentTypes.FIREWORKS);
+
+		if (fireworksComponent == null) {
+			throw new GameTestException("Fireworks component not found on gold nugget");
+		}
+
+		Boolean enchantGlint = Items.GOLD_NUGGET.getComponents().get(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE);
+
+		if (enchantGlint != Boolean.TRUE) {
+			throw new GameTestException("Enchantment glint override not set on gold nugget");
+		}
+
+		context.complete();
+	}
+}
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 627c17dd5..e0c6ab03e 100644
--- a/fabric-item-api-v1/src/testmod/resources/fabric.mod.json
+++ b/fabric-item-api-v1/src/testmod/resources/fabric.mod.json
@@ -11,6 +11,7 @@
   "entrypoints": {
     "main": [
       "net.fabricmc.fabric.test.item.CustomDamageTest",
+      "net.fabricmc.fabric.test.item.DefaultItemComponentTest",
       "net.fabricmc.fabric.test.item.ItemUpdateAnimationTest",
       "net.fabricmc.fabric.test.item.ArmorKnockbackResistanceTest"
     ],
@@ -19,6 +20,7 @@
     ],
     "fabric-gametest" : [
       "net.fabricmc.fabric.test.item.gametest.BrewingStandGameTest",
+      "net.fabricmc.fabric.test.item.gametest.DefaultItemComponentGameTest",
       "net.fabricmc.fabric.test.item.gametest.FurnaceGameTest",
       "net.fabricmc.fabric.test.item.gametest.RecipeGameTest"
     ]