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..cb56713f0cf --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/util/ItemAttributeModifiersBuilder.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +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; +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; + +/** + * 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..67157b0f26d 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()}. @@ -146,115 +141,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..231d6d626a1 100644 --- a/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/ModifyDefaultComponentsEvent.java @@ -5,10 +5,13 @@ 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 +19,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 +28,20 @@ *

* 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 +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 = DataComponentPatch.builder(); + public void modify(ItemLike item, Consumer patch) { + var builder = new ModifyingBuilder(item.asItem().components()); patch.accept(builder); var compPatch = builder.build(); if (!compPatch.isEmpty()) { @@ -71,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)); } @@ -81,4 +90,50 @@ public void modifyMatching(Predicate predicate, Consumer getAllItems() { return BuiltInRegistries.ITEM.stream(); } + + /** + * 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 ModifyingBuilder(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