From 9c3e7bbc12232f5d133e226f819b2d4c52af872f Mon Sep 17 00:00:00 2001 From: ChiefArug <73862885+ChiefArug@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:47:36 +1300 Subject: [PATCH 1/4] Extend ModifyDefaultComponentsEvent to better support changing an items default attribute modifiers. --- .../util/ItemAttributeModifiersBuilder.java | 126 ++++++++++++++++++ .../event/ItemAttributeModifierEvent.java | 117 +--------------- .../event/ModifyDefaultComponentsEvent.java | 66 ++++++++- .../resources/META-INF/accesstransformer.cfg | 6 +- 4 files changed, 192 insertions(+), 123 deletions(-) create mode 100644 src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java diff --git a/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java new file mode 100644 index 00000000000..f38954ee659 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java @@ -0,0 +1,126 @@ +package net.neoforged.neoforge.common.util; + +import com.google.common.collect.ImmutableList; +import net.minecraft.core.Holder; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.EquipmentSlotGroup; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.item.component.ItemAttributeModifiers; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.function.Predicate; + +/** + * Advanced version of {@link ItemAttributeModifiers.Builder} which supports removal and better sanity-checking. + *

+ * The original builder only supports additions and does not guarantee that no duplicate modifiers exist for a given id. + */ +public class ItemAttributeModifiersBuilder { + private List entries; + private Map entriesByKey; + + public ItemAttributeModifiersBuilder(ItemAttributeModifiers defaultModifiers) { + this.entries = new LinkedList<>(); + this.entriesByKey = new HashMap<>(defaultModifiers.modifiers().size()); + + for (ItemAttributeModifiers.Entry entry : defaultModifiers.modifiers()) { + entries.add(entry); + entriesByKey.put(new Key(entry.attribute(), entry.modifier().id()), entry); + } + } + + /** + * Do not use the returned value to create an {@link ItemAttributeModifiers} + * since the underlying list is not immutable, instead use {@link #build}. + * @return an unmodifiable view of the underlying entry list. + */ + public List getEntryView() { + return Collections.unmodifiableList(this.entries); + } + + /** + * Attempts to add a new modifier, refusing if one is already present with the same id. + * + * @return true if the modifier was added + */ + public boolean addModifier(Holder attribute, AttributeModifier modifier, EquipmentSlotGroup slot) { + Key key = new Key(attribute, modifier.id()); + if (entriesByKey.containsKey(key)) { + return false; + } + + ItemAttributeModifiers.Entry entry = new ItemAttributeModifiers.Entry(attribute, modifier, slot); + entries.add(entry); + entriesByKey.put(key, entry); + return true; + } + + /** + * Removes a modifier for the target attribute with the given id. + * + * @return true if a modifier was removed + */ + public boolean removeModifier(Holder attribute, Identifier id) { + ItemAttributeModifiers.Entry entry = entriesByKey.remove(new Key(attribute, id)); + + if (entry != null) { + entries.remove(entry); + return true; + } + + return false; + } + + /** + * Adds a modifier to the list, replacing any existing modifiers with the same id. + * + * @return the previous modifier, or null if there was no previous modifier with the same id + */ + public ItemAttributeModifiers.@Nullable Entry replaceModifier(Holder attribute, AttributeModifier modifier, EquipmentSlotGroup slot) { + Key key = new Key(attribute, modifier.id()); + ItemAttributeModifiers.Entry entry = new ItemAttributeModifiers.Entry(attribute, modifier, slot); + if (entriesByKey.containsKey(key)) { + ItemAttributeModifiers.Entry previousEntry = entriesByKey.get(key); + int index = entries.indexOf(previousEntry); + if (index != -1) { + entries.set(index, entry); + } else { // This should never happen, but it can't hurt to have anyways + entries.add(entry); + } + entriesByKey.put(key, entry); + return previousEntry; + } else { + entries.add(entry); + entriesByKey.put(key, entry); + return null; + } + } + + /** + * Removes modifiers based on a condition. + * + * @return true if any modifiers were removed + */ + public boolean removeIf(Predicate condition) { + this.entries.removeIf(condition); + return this.entriesByKey.values().removeIf(condition); + } + + public void clear() { + this.entries.clear(); + this.entriesByKey.clear(); + } + + public ItemAttributeModifiers build() { + return new ItemAttributeModifiers(ImmutableList.copyOf(this.entries)); + } + + /** + * Internal key class. Attribute modifiers are unique by id for each Attribute. + */ + private record Key(Holder attr, Identifier id) { + + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java b/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java index 81e83e583d7..3493a84ca69 100644 --- a/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java @@ -5,12 +5,7 @@ package net.neoforged.neoforge.event; -import com.google.common.collect.ImmutableList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.function.Predicate; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponents; @@ -21,8 +16,8 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.ItemAttributeModifiers; import net.neoforged.bus.api.Event; +import net.neoforged.neoforge.common.util.ItemAttributeModifiersBuilder; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; /** * This event is fired when the attributes for an item stack are queried (for any reason) through {@link ItemStack#getAttributeModifiers()}. @@ -147,114 +142,4 @@ private ItemAttributeModifiersBuilder getBuilder() { return this.builder; } - /** - * Advanced version of {@link ItemAttributeModifiers.Builder} which supports removal and better sanity-checking. - *

- * The original builder only supports additions and does not guarantee that no duplicate modifiers exist for a given id. - */ - private static class ItemAttributeModifiersBuilder { - private List entries; - private Map entriesByKey; - - ItemAttributeModifiersBuilder(ItemAttributeModifiers defaultModifiers) { - this.entries = new LinkedList<>(); - this.entriesByKey = new HashMap<>(defaultModifiers.modifiers().size()); - - for (ItemAttributeModifiers.Entry entry : defaultModifiers.modifiers()) { - entries.add(entry); - entriesByKey.put(new Key(entry.attribute(), entry.modifier().id()), entry); - } - } - - /** - * {@return an unmodifiable view of the underlying entry list} - */ - List getEntryView() { - return Collections.unmodifiableList(this.entries); - } - - /** - * Attempts to add a new modifier, refusing if one is already present with the same id. - * - * @return true if the modifier was added - */ - boolean addModifier(Holder attribute, AttributeModifier modifier, EquipmentSlotGroup slot) { - Key key = new Key(attribute, modifier.id()); - if (entriesByKey.containsKey(key)) { - return false; - } - - ItemAttributeModifiers.Entry entry = new ItemAttributeModifiers.Entry(attribute, modifier, slot); - entries.add(entry); - entriesByKey.put(key, entry); - return true; - } - - /** - * Removes a modifier for the target attribute with the given id. - * - * @return true if a modifier was removed - */ - boolean removeModifier(Holder attribute, Identifier id) { - ItemAttributeModifiers.Entry entry = entriesByKey.remove(new Key(attribute, id)); - - if (entry != null) { - entries.remove(entry); - return true; - } - - return false; - } - - /** - * Adds a modifier to the list, replacing any existing modifiers with the same id. - * - * @return the previous modifier, or null if there was no previous modifier with the same id - */ - ItemAttributeModifiers.@Nullable Entry replaceModifier(Holder attribute, AttributeModifier modifier, EquipmentSlotGroup slot) { - Key key = new Key(attribute, modifier.id()); - ItemAttributeModifiers.Entry entry = new ItemAttributeModifiers.Entry(attribute, modifier, slot); - if (entriesByKey.containsKey(key)) { - ItemAttributeModifiers.Entry previousEntry = entriesByKey.get(key); - int index = entries.indexOf(previousEntry); - if (index != -1) { - entries.set(index, entry); - } else { // This should never happen, but it can't hurt to have anyways - entries.add(entry); - } - entriesByKey.put(key, entry); - return previousEntry; - } else { - entries.add(entry); - entriesByKey.put(key, entry); - return null; - } - } - - /** - * Removes modifiers based on a condition. - * - * @return true if any modifiers were removed - */ - boolean removeIf(Predicate condition) { - this.entries.removeIf(condition); - return this.entriesByKey.values().removeIf(condition); - } - - void clear() { - this.entries.clear(); - this.entriesByKey.clear(); - } - - public ItemAttributeModifiers build() { - return new ItemAttributeModifiers(ImmutableList.copyOf(this.entries)); - } - - /** - * Internal key class. Attribute modifiers are unique by id for each Attribute. - */ - private static record Key(Holder attr, Identifier id) { - - } - } } diff --git a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java index 68146cf5c2d..a0c4782a9a5 100644 --- a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java @@ -5,10 +5,14 @@ package net.neoforged.neoforge.event; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; + +import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.Item; import net.minecraft.world.level.ItemLike; @@ -16,6 +20,7 @@ import net.neoforged.bus.api.EventPriority; import net.neoforged.fml.ModContainer; import net.neoforged.fml.event.IModBusEvent; +import net.neoforged.neoforge.common.CommonHooks; import org.jetbrains.annotations.ApiStatus; /** @@ -24,15 +29,22 @@ *

* Example usage: * {@snippet : - * import net.minecraft.core.component.DataComponents; - * import net.minecraft.world.item.Items; - * * public void modifyComponents(ModifyDefaultComponentsEvent event) { * event.modify(Items.MELON_SEEDS, builder -> builder * .set(DataComponents.MAX_STACK_SIZE, 16)); // Stack melon seeds to at most 16 items * * event.modify(Items.APPLE, builder -> builder * .remove(DataComponents.FOOD)); // Remove the ability of eating apples + * + * event.modify(Items.GOLDEN_SWORD, builder -> + * builder.modify(DataComponents.ATTRIBUTE_MODIFIERS, orig -> { + * var attributes = new ItemAttributeModifiersBuilder(orig); + * attributes.replaceModifier(Attributes.ATTACK_DAMAGE, // Change golden swords base attack damage to +7 + * new AttributeModifier(Item.BASE_ATTACK_DAMAGE_ID, 7, AttributeModifier.Operation.ADD_VALUE), + * EquipmentSlotGroup.MAINHAND); + * return attributes.build(); + * }); + * ); * } * * // Lowest priority listener @@ -52,8 +64,8 @@ public ModifyDefaultComponentsEvent() {} * @param item the item to modify the default components for * @param patch the patch to apply */ - public void modify(ItemLike item, Consumer patch) { - var builder = DataComponentPatch.builder(); + public void modify(ItemLike item, Consumer patch) { + var builder = new Builder(item.asItem().components()); patch.accept(builder); var compPatch = builder.build(); if (!compPatch.isEmpty()) { @@ -71,7 +83,7 @@ public void modify(ItemLike item, Consumer patch) { * @param predicate the item filter * @param patch the patch to apply */ - public void modifyMatching(Predicate predicate, Consumer patch) { + public void modifyMatching(Predicate predicate, Consumer patch) { getAllItems().filter(predicate).forEach(item -> modify(item, patch)); } @@ -81,4 +93,46 @@ public void modifyMatching(Predicate predicate, Consumer getAllItems() { return BuiltInRegistries.ITEM.stream(); } + + public static class Builder extends DataComponentPatch.Builder { + private final DataComponentMap base; + + private Builder(DataComponentMap baseComponents) { + base = baseComponents; + } + + /** + * Modify an existing component. {@code modification} will only be called if the data component is present in + * the builder or in the base item's components, and has not been removed by another modification. + * To be able to remove a component, use {@link #modifyOptional} + */ + @SuppressWarnings("unchecked") // Safe provided the map keeps the guarantee that a DCT maps to an Optional + public DataComponentPatch.Builder modify(DataComponentType type, java.util.function.UnaryOperator modification) { + if (map.containsKey(type)) { + Optional val = ((Optional) map.get(type)).map(modification); + val.ifPresent(CommonHooks::validateComponent); + map.put(type, val); + } else { + T val = base.get(type); + if (val != null) { + val = modification.apply(val); + CommonHooks.validateComponent(val); + map.put(type, Optional.of(val)); + } + } + return this; + } + + /** + * Modify a component, whether it's already present or not. Return {@link Optional#empty} to remove the component. + */ + @SuppressWarnings("unchecked") // Safe provided the map keeps the guarantee that a DCT maps to an Optional + public DataComponentPatch.Builder modifyOptional(DataComponentType type, java.util.function.UnaryOperator> modification) { + Optional val = map.containsKey(type) ? (Optional) map.get(type) : Optional.ofNullable(base.get(type)); + val = modification.apply(val); + val.ifPresent(CommonHooks::validateComponent); + map.put(type, val); + return this; + } + } } diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 103d36dc719..980ca10acf9 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -343,4 +343,8 @@ public net.minecraft.client.gui.components.ChatComponent getScale()D public net.minecraft.client.gui.screens.worldselection.EditGameRulesScreen markInvalid(Lnet/minecraft/client/gui/screens/worldselection/EditGameRulesScreen$RuleEntry;)V public net.minecraft.client.gui.screens.worldselection.EditGameRulesScreen clearInvalid(Lnet/minecraft/client/gui/screens/worldselection/EditGameRulesScreen$RuleEntry;)V public net.minecraft.client.gui.screens.worldselection.EditGameRulesScreen gameRules -public net.minecraft.client.gui.screens.worldselection.EditGameRulesScreen$EntryFactory \ No newline at end of file +public net.minecraft.client.gui.screens.worldselection.EditGameRulesScreen$EntryFactory + +# For extending for an enhanced builder in the ModifyDefaultComponentsEvent +protected net.minecraft.core.component.DataComponentPatch$Builder ()V +protected net.minecraft.core.component.DataComponentPatch$Builder map \ No newline at end of file From ae1323b6aa72b0eb9aad4ecfef0d540b84884e44 Mon Sep 17 00:00:00 2001 From: ChiefArug <73862885+ChiefArug@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:58:16 +1300 Subject: [PATCH 2/4] Fix formatting --- .../neoforge/common/util/ItemAttributeModifiersBuilder.java | 5 +++++ .../neoforged/neoforge/event/ItemAttributeModifierEvent.java | 1 - .../neoforge/event/ModifyDefaultComponentsEvent.java | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java index f38954ee659..71b79c22966 100644 --- a/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java +++ b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforge.common.util; import com.google.common.collect.ImmutableList; diff --git a/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java b/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java index 3493a84ca69..67157b0f26d 100644 --- a/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ItemAttributeModifierEvent.java @@ -141,5 +141,4 @@ private ItemAttributeModifiersBuilder getBuilder() { return this.builder; } - } diff --git a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java index a0c4782a9a5..bb5a31e243e 100644 --- a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java @@ -9,7 +9,6 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; - import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponentType; @@ -43,7 +42,7 @@ * new AttributeModifier(Item.BASE_ATTACK_DAMAGE_ID, 7, AttributeModifier.Operation.ADD_VALUE), * EquipmentSlotGroup.MAINHAND); * return attributes.build(); - * }); + * }) * ); * } * From 5dd5159ec6261060398267eaa206208875c6f4da Mon Sep 17 00:00:00 2001 From: ChiefArug <73862885+ChiefArug@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:08:48 +1300 Subject: [PATCH 3/4] More formatting that didn't apply the first time --- .../util/ItemAttributeModifiersBuilder.java | 10 +++++++--- .../event/ModifyDefaultComponentsEvent.java | 16 +++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java index 71b79c22966..cb56713f0cf 100644 --- a/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java +++ b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java @@ -6,6 +6,12 @@ package net.neoforged.neoforge.common.util; import com.google.common.collect.ImmutableList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; import net.minecraft.core.Holder; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.EquipmentSlotGroup; @@ -14,9 +20,6 @@ import net.minecraft.world.item.component.ItemAttributeModifiers; import org.jspecify.annotations.Nullable; -import java.util.*; -import java.util.function.Predicate; - /** * Advanced version of {@link ItemAttributeModifiers.Builder} which supports removal and better sanity-checking. *

@@ -39,6 +42,7 @@ public ItemAttributeModifiersBuilder(ItemAttributeModifiers defaultModifiers) { /** * Do not use the returned value to create an {@link ItemAttributeModifiers} * since the underlying list is not immutable, instead use {@link #build}. + * * @return an unmodifiable view of the underlying entry list. */ public List getEntryView() { diff --git a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java index bb5a31e243e..4eff84f122f 100644 --- a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java @@ -35,15 +35,13 @@ * event.modify(Items.APPLE, builder -> builder * .remove(DataComponents.FOOD)); // Remove the ability of eating apples * - * event.modify(Items.GOLDEN_SWORD, builder -> - * builder.modify(DataComponents.ATTRIBUTE_MODIFIERS, orig -> { - * var attributes = new ItemAttributeModifiersBuilder(orig); - * attributes.replaceModifier(Attributes.ATTACK_DAMAGE, // Change golden swords base attack damage to +7 - * new AttributeModifier(Item.BASE_ATTACK_DAMAGE_ID, 7, AttributeModifier.Operation.ADD_VALUE), - * EquipmentSlotGroup.MAINHAND); - * return attributes.build(); - * }) - * ); + * event.modify(Items.GOLDEN_SWORD, builder -> builder.modify(DataComponents.ATTRIBUTE_MODIFIERS, orig -> { + * var attributes = new ItemAttributeModifiersBuilder(orig); + * attributes.replaceModifier(Attributes.ATTACK_DAMAGE, // Change golden swords base attack damage to +7 + * new AttributeModifier(Item.BASE_ATTACK_DAMAGE_ID, 7, AttributeModifier.Operation.ADD_VALUE), + * EquipmentSlotGroup.MAINHAND); + * return attributes.build(); + * })); * } * * // Lowest priority listener From 00fd720145053c71c86dfcfe4707a394b355a6ef Mon Sep 17 00:00:00 2001 From: ChiefArug <73862885+ChiefArug@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:50:40 +1300 Subject: [PATCH 4/4] Rename and document extended DataComponentPatch builder --- .../event/ModifyDefaultComponentsEvent.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java index 4eff84f122f..231d6d626a1 100644 --- a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java @@ -61,8 +61,8 @@ public ModifyDefaultComponentsEvent() {} * @param item the item to modify the default components for * @param patch the patch to apply */ - public void modify(ItemLike item, Consumer patch) { - var builder = new Builder(item.asItem().components()); + public void modify(ItemLike item, Consumer patch) { + var builder = new ModifyingBuilder(item.asItem().components()); patch.accept(builder); var compPatch = builder.build(); if (!compPatch.isEmpty()) { @@ -80,7 +80,7 @@ public void modify(ItemLike item, Consumer patch) { * @param predicate the item filter * @param patch the patch to apply */ - public void modifyMatching(Predicate predicate, Consumer patch) { + public void modifyMatching(Predicate predicate, Consumer patch) { getAllItems().filter(predicate).forEach(item -> modify(item, patch)); } @@ -91,10 +91,14 @@ public Stream getAllItems() { return BuiltInRegistries.ITEM.stream(); } - public static class Builder extends DataComponentPatch.Builder { + /** + * An extension of the vanilla builder that includes some methods for modifying components either already in + * the builder or from a base set of components. + */ + public static class ModifyingBuilder extends DataComponentPatch.Builder { private final DataComponentMap base; - private Builder(DataComponentMap baseComponents) { + private ModifyingBuilder(DataComponentMap baseComponents) { base = baseComponents; }