diff --git a/patches/net/minecraft/world/entity/ai/attributes/Attribute.java.patch b/patches/net/minecraft/world/entity/ai/attributes/Attribute.java.patch index 8e3ac33ea3..3863a37e98 100644 --- a/patches/net/minecraft/world/entity/ai/attributes/Attribute.java.patch +++ b/patches/net/minecraft/world/entity/ai/attributes/Attribute.java.patch @@ -9,3 +9,25 @@ public static final Codec> CODEC = BuiltInRegistries.ATTRIBUTE.holderByNameCodec(); public static final StreamCodec> STREAM_CODEC = ByteBufCodecs.holderRegistry(Registries.ATTRIBUTE); private final double defaultValue; +@@ -50,6 +_,21 @@ + + public ChatFormatting getStyle(boolean p_347715_) { + return this.sentiment.getStyle(p_347715_); ++ } ++ ++ // Neo: Patch in the default implementation of IAttributeExtension#getMergedStyle since we need access to Attribute#sentiment ++ ++ protected static final net.minecraft.network.chat.TextColor MERGED_RED = net.minecraft.network.chat.TextColor.fromRgb(0xF93131); ++ protected static final net.minecraft.network.chat.TextColor MERGED_BLUE = net.minecraft.network.chat.TextColor.fromRgb(0x7A7AF9); ++ protected static final net.minecraft.network.chat.TextColor MERGED_GRAY = net.minecraft.network.chat.TextColor.fromRgb(0xCCCCCC); ++ ++ @Override ++ public net.minecraft.network.chat.TextColor getMergedStyle(boolean isPositive) { ++ return switch (this.sentiment) { ++ case POSITIVE -> isPositive ? MERGED_BLUE : MERGED_RED; ++ case NEGATIVE -> isPositive ? MERGED_RED : MERGED_BLUE; ++ case NEUTRAL -> MERGED_GRAY; ++ }; + } + + public static enum Sentiment { diff --git a/patches/net/minecraft/world/item/ItemStack.java.patch b/patches/net/minecraft/world/item/ItemStack.java.patch index ae0f9ec7be..0ae95d06e1 100644 --- a/patches/net/minecraft/world/item/ItemStack.java.patch +++ b/patches/net/minecraft/world/item/ItemStack.java.patch @@ -148,13 +148,14 @@ if (this.has(DataComponents.CUSTOM_NAME)) { mutablecomponent.withStyle(ChatFormatting.ITALIC); } -@@ -752,7 +_,8 @@ +@@ -752,7 +_,9 @@ this.addToTooltip(DataComponents.ENCHANTMENTS, p_339637_, consumer, p_41653_); this.addToTooltip(DataComponents.DYED_COLOR, p_339637_, consumer, p_41653_); this.addToTooltip(DataComponents.LORE, p_339637_, consumer, p_41653_); - this.addAttributeTooltips(consumer, p_41652_); + // Neo: Replace attribute tooltips with custom handling -+ net.neoforged.neoforge.client.util.TooltipUtil.addAttributeTooltips(p_41652_, this, consumer, p_41653_); ++ net.neoforged.neoforge.client.util.TooltipUtil.addAttributeTooltips(this, consumer, ++ net.neoforged.neoforge.client.util.TooltipUtil.AttributeTooltipContext.of(p_41652_, p_339637_, p_41653_)); this.addToTooltip(DataComponents.UNBREAKABLE, p_339637_, consumer, p_41653_); AdventureModePredicate adventuremodepredicate = this.get(DataComponents.CAN_BREAK); if (adventuremodepredicate != null && adventuremodepredicate.showInTooltip()) { diff --git a/src/main/java/net/neoforged/neoforge/client/event/AddAttributeTooltipsEvent.java b/src/main/java/net/neoforged/neoforge/client/event/AddAttributeTooltipsEvent.java index 67af2d5f7c..64db9c1835 100644 --- a/src/main/java/net/neoforged/neoforge/client/event/AddAttributeTooltipsEvent.java +++ b/src/main/java/net/neoforged/neoforge/client/event/AddAttributeTooltipsEvent.java @@ -6,36 +6,38 @@ package net.neoforged.neoforge.client.event; import java.util.function.Consumer; +import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.ItemAttributeModifiers; import net.neoforged.api.distmarker.Dist; -import net.neoforged.neoforge.event.entity.player.PlayerEvent; -import org.jetbrains.annotations.Nullable; +import net.neoforged.bus.api.Event; +import net.neoforged.neoforge.client.util.TooltipUtil; +import net.neoforged.neoforge.client.util.TooltipUtil.AttributeTooltipContext; /** - * This event is used to add additional attribute tooltip lines without having to manually locate the inject point. + * This event is fired after attribute tooltip lines have been added to an item stack's tooltip in {@link TooltipUtil#addAttributeTooltips}. + *

+ * It can be used to add additional tooltip lines adjacent to the attribute lines without having to manually locate the inject point. *

* This event is only fired on the {@linkplain Dist#CLIENT physical client}. */ -public class AddAttributeTooltipsEvent extends PlayerEvent { +public class AddAttributeTooltipsEvent extends Event { protected final ItemStack stack; protected final Consumer tooltip; - protected final TooltipFlag flag; + protected final AttributeTooltipContext ctx; - public AddAttributeTooltipsEvent(ItemStack stack, @Nullable Player player, Consumer tooltip, TooltipFlag flag) { - super(player); + public AddAttributeTooltipsEvent(ItemStack stack, Consumer tooltip, AttributeTooltipContext ctx) { this.stack = stack; this.tooltip = tooltip; - this.flag = flag; + this.ctx = ctx; } /** - * Use to determine if the advanced information on item tooltips is being shown, toggled by F3+H. + * The current tooltip context. */ - public TooltipFlag getFlags() { - return this.flag; + public AttributeTooltipContext getContext() { + return this.ctx; } /** @@ -46,18 +48,20 @@ public ItemStack getStack() { } /** - * Adds a single {@link Component} to the itemstack's tooltip. + * Adds one or more {@link Component}s to the tooltip. */ - public void addTooltipLine(Component comp) { - this.tooltip.accept(comp); + public void addTooltipLines(Component... comps) { + for (Component comp : comps) { + this.tooltip.accept(comp); + } } /** - * This event is fired with a null player during startup when populating search trees for tooltips. + * Checks if the attribute tooltips should be shown on the current item stack. + *

+ * This event is fired even if the component would prevent the normal tooltip lines from showing. */ - @Override - @Nullable - public Player getEntity() { - return super.getEntity(); + public boolean shouldShow() { + return this.stack.getOrDefault(DataComponents.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.EMPTY).showInTooltip(); } } diff --git a/src/main/java/net/neoforged/neoforge/client/event/GatherSkippedAttributeTooltipsEvent.java b/src/main/java/net/neoforged/neoforge/client/event/GatherSkippedAttributeTooltipsEvent.java index c53ae4d8f1..32f20ea6fc 100644 --- a/src/main/java/net/neoforged/neoforge/client/event/GatherSkippedAttributeTooltipsEvent.java +++ b/src/main/java/net/neoforged/neoforge/client/event/GatherSkippedAttributeTooltipsEvent.java @@ -7,37 +7,35 @@ import java.util.Set; import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.TooltipFlag; import net.neoforged.api.distmarker.Dist; -import net.neoforged.neoforge.event.entity.player.PlayerEvent; -import org.jetbrains.annotations.Nullable; +import net.neoforged.bus.api.Event; +import net.neoforged.neoforge.client.util.TooltipUtil.AttributeTooltipContext; /** - * This event is used to collect UUIDs of attribute modifiers that will not be displayed in item tooltips. + * This event is used to collect the IDs of attribute modifiers that will not be displayed in item tooltips. *

- * This allows hiding specific modifiers for whatever reason. They will still be shown in the attributes GUI. + * It allows hiding some (or all) of the modifiers, potentially for displaying them in an alternative way (or for hiding information from the player). *

* This event is only fired on the {@linkplain Dist#CLIENT physical client}. */ -public class GatherSkippedAttributeTooltipsEvent extends PlayerEvent { +public class GatherSkippedAttributeTooltipsEvent extends Event { protected final ItemStack stack; protected final Set skips; - protected final TooltipFlag flag; + protected final AttributeTooltipContext ctx; + protected boolean skipAll = false; - public GatherSkippedAttributeTooltipsEvent(ItemStack stack, @Nullable Player player, Set skips, TooltipFlag flag) { - super(player); + public GatherSkippedAttributeTooltipsEvent(ItemStack stack, Set skips, AttributeTooltipContext ctx) { this.stack = stack; this.skips = skips; - this.flag = flag; + this.ctx = ctx; } /** - * Use to determine if the advanced information on item tooltips is being shown, toggled by F3+H. + * The current tooltip context. */ - public TooltipFlag getFlags() { - return this.flag; + public AttributeTooltipContext getContext() { + return this.ctx; } /** @@ -48,18 +46,30 @@ public ItemStack getStack() { } /** - * Mark the ResourceLocation of a specific attribute modifier as skipped, causing it to not be displayed in the tooltip. + * Marks the id of a specific attribute modifier as skipped, causing it to not be displayed in the tooltip. */ - public void skipID(ResourceLocation id) { + public void skipId(ResourceLocation id) { this.skips.add(id); } /** - * This event is fired with a null player during startup when populating search trees for tooltips. + * Checks if a given id is skipped or not. If all modifiers are skipped, this method always returns true. */ - @Override - @Nullable - public Player getEntity() { - return super.getEntity(); + public boolean isSkipped(ResourceLocation id) { + return this.skipAll || this.skips.contains(id); + } + + /** + * Sets if the event should skip displaying all attribute modifiers. + */ + public void setSkipAll(boolean skip) { + this.skipAll = skip; + } + + /** + * Checks if the event will cause all attribute modifiers to be skipped. + */ + public boolean isSkippingAll() { + return this.skipAll; } } diff --git a/src/main/java/net/neoforged/neoforge/client/util/TooltipUtil.java b/src/main/java/net/neoforged/neoforge/client/util/TooltipUtil.java index 1dc8922c58..5c4de24d14 100644 --- a/src/main/java/net/neoforged/neoforge/client/util/TooltipUtil.java +++ b/src/main/java/net/neoforged/neoforge/client/util/TooltipUtil.java @@ -14,11 +14,13 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.screens.Screen; import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup.Provider; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; @@ -29,126 +31,212 @@ import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Item.TooltipContext; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.component.ItemAttributeModifiers; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.maps.MapId; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; +import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.neoforge.client.event.AddAttributeTooltipsEvent; import net.neoforged.neoforge.client.event.GatherSkippedAttributeTooltipsEvent; import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.common.NeoForgeMod; +import net.neoforged.neoforge.common.extensions.IAttributeExtension; import net.neoforged.neoforge.common.util.AttributeUtil; import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; import org.jetbrains.annotations.Nullable; +/** + * Tooltip generating utility code to support {@link IAttributeExtension}. + * + * @see {@link AttributeUtil} for additional non-tooltip code + */ public class TooltipUtil { public static final ResourceLocation FAKE_MERGED_ID = ResourceLocation.fromNamespaceAndPath(NeoForgeVersion.MOD_ID, "fake_merged_modifier"); - public static void addAttributeTooltips(@Nullable Player player, ItemStack stack, Consumer tooltip, TooltipFlag flag) { + /** + * Checks if attribute modifer tooltips should show, and if they should, adds tooltips for all attribute modifiers present on an item stack to the stack's tooltip lines. + *

+ * After the tooltip lines have been added, fires the {@link AddAttributeTooltipsEvent} to allow mods to add additional attribute-related lines. + * + * @param tooltip A consumer to add the tooltip lines to. + * @param ctx The tooltip context + */ + public static void addAttributeTooltips(ItemStack stack, Consumer tooltip, AttributeTooltipContext ctx) { ItemAttributeModifiers modifiers = stack.getOrDefault(DataComponents.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.EMPTY); if (modifiers.showInTooltip()) { - applyModifierTooltips(player, stack, tooltip, flag); + applyModifierTooltips(stack, tooltip, ctx); } - NeoForge.EVENT_BUS.post(new AddAttributeTooltipsEvent(stack, player, tooltip, flag)); + NeoForge.EVENT_BUS.post(new AddAttributeTooltipsEvent(stack, tooltip, ctx)); } - public static void applyModifierTooltips(@Nullable Player player, ItemStack stack, Consumer tooltip, TooltipFlag flag) { + /** + * Unconditionally applies the attribute modifier tooltips for all attribute modifiers present on the item stack. + *

+ * Before application, this method posts the {@link GatherSkippedAttributeTooltipsEvent} to determine which tooltips should be skipped. + * + * @param stack + * @param tooltip + * @param ctx + */ + public static void applyModifierTooltips(ItemStack stack, Consumer tooltip, AttributeTooltipContext ctx) { Set skips = new HashSet<>(); - NeoForge.EVENT_BUS.post(new GatherSkippedAttributeTooltipsEvent(stack, player, skips, flag)); + var event = NeoForge.EVENT_BUS.post(new GatherSkippedAttributeTooltipsEvent(stack, skips, ctx)); + if (event.isSkippingAll()) { + return; + } for (EquipmentSlotGroup group : EquipmentSlotGroup.values()) { Multimap, AttributeModifier> modifiers = AttributeUtil.getSortedModifiers(stack, group); - applyTextFor(player, stack, tooltip, modifiers, group, skips, flag); + Component groupName = Component.translatable("item.modifiers." + group.getSerializedName()).withStyle(ChatFormatting.GRAY); + applyTextFor(stack, tooltip, modifiers, groupName, skips, ctx); } } - public static void applyTextFor(@Nullable Player player, ItemStack stack, Consumer tooltip, Multimap, AttributeModifier> modifierMap, - EquipmentSlotGroup group, Set skips, TooltipFlag flag) { - if (!modifierMap.isEmpty()) { - modifierMap.values().removeIf(m -> skips.contains(m.id())); + /** + * Applies the text for a single group of modifiers to the tooltip for a given item stack. + *

+ * This method will attempt to merge multiple modifiers for a single attribute into a single modifier if {@linkplain NeoForgeMod#enableMergedAttributeTooltips()} was called. + * + * @param stack The item stack that owns the modifiers + * @param tooltip The consumer to append tooltip components to + * @param modifierMap A mutable map of modifiers for the given group + * @param groupName The name of the current modifier group + * @param skips A set of modifier IDs to not apply to the tooltip + * @param ctx The tooltip context + */ + public static void applyTextFor(ItemStack stack, Consumer tooltip, Multimap, AttributeModifier> modifierMap, Component groupName, Set skips, AttributeTooltipContext ctx) { + // Remove any skipped modifiers before doing any logic + modifierMap.values().removeIf(m -> skips.contains(m.id())); - tooltip.accept(Component.empty()); - tooltip.accept(Component.translatable("item.modifiers." + group.getSerializedName()).withStyle(ChatFormatting.GRAY)); + // Don't add anything if there is nothing in the group + if (modifierMap.isEmpty()) { + return; + } - if (modifierMap.isEmpty()) return; + // Add an empty line, then the name of the group + tooltip.accept(Component.empty()); + tooltip.accept(groupName); - Map, BaseModifier> baseModifs = new IdentityHashMap<>(); + // Collect all the base modifiers + Map, BaseModifier> baseModifs = new IdentityHashMap<>(); - modifierMap.forEach((attr, modif) -> { - if (modif.id().equals(attr.value().getBaseID())) { - baseModifs.put(attr, new BaseModifier(modif, new ArrayList<>())); - } - }); + var it = modifierMap.entries().iterator(); + while (it.hasNext()) { + Entry, AttributeModifier> entry = it.next(); + Holder attr = entry.getKey(); + AttributeModifier modif = entry.getValue(); + if (modif.id().equals(attr.value().getBaseId())) { + baseModifs.put(attr, new BaseModifier(modif, new ArrayList<>())); + // Remove base modifiers from the main map after collection so we don't need to check for them later. + it.remove(); + } + } - modifierMap.forEach((attr, modif) -> { - BaseModifier base = baseModifs.get(attr); - if (base != null && base.base != modif) { - base.children.add(modif); - } - }); - - for (Map.Entry, BaseModifier> entry : baseModifs.entrySet()) { - Holder attr = entry.getKey(); - BaseModifier baseModif = entry.getValue(); - double entityBase = player == null ? 0 : player.getAttributeBaseValue(attr); - double base = baseModif.base.amount() + entityBase; - final double rawBase = base; - double amt = base; - double baseBonus = attr.value().getBonusBaseValue(stack); + // Collect children of all base modifiers for merging logic + modifierMap.forEach((attr, modif) -> { + BaseModifier base = baseModifs.get(attr); + if (base != null) { + base.children.add(modif); + } + }); + + // Add tooltip lines for base modifiers + for (Map.Entry, BaseModifier> entry : baseModifs.entrySet()) { + Holder attr = entry.getKey(); + BaseModifier baseModif = entry.getValue(); + double entityBase = ctx.player() == null ? 0 : ctx.player().getAttributeBaseValue(attr); + double base = baseModif.base.amount() + entityBase; + final double rawBase = base; + double amt = base; + + // Compute the base value including merged modifiers if merging is enabled + if (NeoForgeMod.shouldMergeAttributeTooltips()) { for (AttributeModifier modif : baseModif.children) { - if (modif.operation() == Operation.ADD_VALUE) base = amt = amt + modif.amount(); - else if (modif.operation() == Operation.ADD_MULTIPLIED_BASE) amt += modif.amount() * base; - else amt *= 1 + modif.amount(); - } - amt += baseBonus; - boolean isMerged = !baseModif.children.isEmpty() || baseBonus != 0; - MutableComponent text = attr.value().toBaseComponent(amt, entityBase, isMerged, flag); - tooltip.accept(padded(" ", text).withStyle(isMerged ? ChatFormatting.GOLD : ChatFormatting.DARK_GREEN)); - if (Screen.hasShiftDown() && isMerged) { - // Display the raw base value, and then all children modifiers. - text = attr.value().toBaseComponent(rawBase, entityBase, false, flag); - tooltip.accept(list().append(text.withStyle(ChatFormatting.DARK_GREEN))); - for (AttributeModifier modifier : baseModif.children) { - tooltip.accept(list().append(attr.value().toComponent(modifier, flag))); - } - if (baseBonus > 0) { - attr.value().addBonusTooltips(stack, tooltip, flag); + switch (modif.operation()) { + case ADD_VALUE: + base = amt = amt + modif.amount(); + break; + case ADD_MULTIPLIED_BASE: + amt += modif.amount() * base; + break; + case ADD_MULTIPLIED_TOTAL: + amt *= 1 + modif.amount(); + break; } } } - for (Holder attr : modifierMap.keySet()) { - if (baseModifs.containsKey(attr)) continue; - Collection modifs = modifierMap.get(attr); - // Initiate merged-tooltip logic if we have more than one modifier for a given attribute. - if (modifs.size() > 1) { - double[] sums = new double[3]; - boolean[] merged = new boolean[3]; - Map> shiftExpands = new HashMap<>(); - for (AttributeModifier modifier : modifs) { - if (modifier.amount() == 0) continue; - if (sums[modifier.operation().ordinal()] != 0) merged[modifier.operation().ordinal()] = true; - sums[modifier.operation().ordinal()] += modifier.amount(); - shiftExpands.computeIfAbsent(modifier.operation(), k -> new LinkedList<>()).add(modifier); + double baseBonus = attr.value().getBonusBaseValue(stack); + amt += baseBonus; + + boolean isMerged = NeoForgeMod.shouldMergeAttributeTooltips() && (!baseModif.children.isEmpty() || baseBonus != 0); + MutableComponent text = attr.value().toBaseComponent(amt, entityBase, isMerged, ctx.flag()); + tooltip.accept(padded(" ", text).withStyle(isMerged ? ChatFormatting.GOLD : ChatFormatting.DARK_GREEN)); + if (FMLEnvironment.dist.isClient() && Screen.hasShiftDown() && isMerged) { + // Display the raw base value, and then all children modifiers. + text = attr.value().toBaseComponent(rawBase, entityBase, false, ctx.flag()); + tooltip.accept(list().append(text.withStyle(ChatFormatting.DARK_GREEN))); + for (AttributeModifier modifier : baseModif.children) { + tooltip.accept(list().append(attr.value().toComponent(modifier, ctx.flag()))); + } + if (baseBonus > 0) { + attr.value().addBonusTooltips(stack, tooltip, ctx.flag()); + } + } + } + + for (Holder attr : modifierMap.keySet()) { + // Skip attributes who have already been processed during the base modifier stage + if (NeoForgeMod.shouldMergeAttributeTooltips() && baseModifs.containsKey(attr)) { + continue; + } + + Collection modifs = modifierMap.get(attr); + // Initiate merged-tooltip logic if we have more than one modifier for a given attribute. + if (NeoForgeMod.shouldMergeAttributeTooltips() && modifs.size() > 1) { + double[] sums = new double[3]; + boolean[] merged = new boolean[3]; + Map> shiftExpands = new HashMap<>(); + + for (AttributeModifier modifier : modifs) { + if (modifier.amount() == 0) continue; + if (sums[modifier.operation().ordinal()] != 0) merged[modifier.operation().ordinal()] = true; + sums[modifier.operation().ordinal()] += modifier.amount(); + shiftExpands.computeIfAbsent(modifier.operation(), k -> new LinkedList<>()).add(modifier); + } + + for (Operation op : Operation.values()) { + int i = op.ordinal(); + + // If the merged value comes out to 0, just ignore the whole stack + if (sums[i] == 0) { + continue; } - for (Operation op : Operation.values()) { - int i = op.ordinal(); - if (sums[i] == 0) continue; - if (merged[i]) { - TextColor color = sums[i] < 0 ? TextColor.fromRgb(0xF93131) : TextColor.fromRgb(0x7A7AF9); - if (sums[i] < 0) sums[i] *= -1; - var fakeModif = new AttributeModifier(FAKE_MERGED_ID, sums[i], op); - MutableComponent comp = attr.value().toComponent(fakeModif, flag); - tooltip.accept(comp.withStyle(comp.getStyle().withColor(color))); - if (merged[i] && Screen.hasShiftDown()) { - shiftExpands.get(Operation.BY_ID.apply(i)).forEach(modif -> tooltip.accept(list().append(attr.value().toComponent(modif, flag)))); - } - } else { - var fakeModif = new AttributeModifier(FAKE_MERGED_ID, sums[i], op); - tooltip.accept(attr.value().toComponent(fakeModif, flag)); + + // Handle merged modifier stacks by creating a "fake" merged modifier with the underlying value. + if (merged[i]) { + TextColor color = attr.value().getMergedStyle(sums[i] > 0); + var fakeModif = new AttributeModifier(FAKE_MERGED_ID, sums[i], op); + MutableComponent comp = attr.value().toComponent(fakeModif, ctx.flag()); + tooltip.accept(comp.withStyle(comp.getStyle().withColor(color))); + if (FMLEnvironment.dist.isClient() && Screen.hasShiftDown() && merged[i]) { + shiftExpands.get(Operation.BY_ID.apply(i)).forEach(modif -> tooltip.accept(list().append(attr.value().toComponent(modif, ctx.flag())))); } + } else { + var fakeModif = new AttributeModifier(FAKE_MERGED_ID, sums[i], op); + tooltip.accept(attr.value().toComponent(fakeModif, ctx.flag())); + } + } + } else { + for (AttributeModifier m : modifs) { + if (m.amount() != 0) { + tooltip.accept(attr.value().toComponent(m, ctx.flag())); } - } else modifs.forEach(m -> { - if (m.amount() != 0) tooltip.accept(attr.value().toComponent(m, flag)); - }); + } } } } @@ -168,4 +256,55 @@ private static MutableComponent padded(String padding, Component comp) { } public static record BaseModifier(AttributeModifier base, List children) {} + + /** + * Extended {@link TooltipContext} used when generating attribute tooltips. + */ + public static interface AttributeTooltipContext extends Item.TooltipContext { + /** + * {@return the player for whom tooltips are being generated for, if known} + */ + @Nullable + Player player(); + + /** + * {@return the current tooltip flag} + */ + TooltipFlag flag(); + + public static AttributeTooltipContext of(@Nullable Player player, Item.TooltipContext itemCtx, TooltipFlag flag) { + return new AttributeTooltipContext() { + @Override + public Provider registries() { + return itemCtx.registries(); + } + + @Override + public float tickRate() { + return itemCtx.tickRate(); + } + + @Override + public MapItemSavedData mapData(MapId id) { + return itemCtx.mapData(id); + } + + @Override + public Level level() { + return itemCtx.level(); + } + + @Nullable + @Override + public Player player() { + return player; + } + + @Override + public TooltipFlag flag() { + return flag; + } + }; + } + } } diff --git a/src/main/java/net/neoforged/neoforge/common/BooleanAttribute.java b/src/main/java/net/neoforged/neoforge/common/BooleanAttribute.java index d538aa626d..8ae5ec0ea2 100644 --- a/src/main/java/net/neoforged/neoforge/common/BooleanAttribute.java +++ b/src/main/java/net/neoforged/neoforge/common/BooleanAttribute.java @@ -44,28 +44,26 @@ public double sanitizeValue(double value) { @Override public MutableComponent toValueComponent(@Nullable Operation op, double value, TooltipFlag flag) { if (op == null) { - return Component.translatable("apothic_attributes.value.boolean." + (value > 0 ? "enabled" : "disabled")); + return Component.translatable("neoforge.value.boolean." + (value > 0 ? "enabled" : "disabled")); } else if (op == Operation.ADD_VALUE && value > 0) { - return Component.translatable("apothic_attributes.value.boolean.enable"); + return Component.translatable("neoforge.value.boolean.enable"); } else if (op == Operation.ADD_MULTIPLIED_TOTAL && (int) value == -1) { - return Component.translatable("apothic_attributes.value.boolean.force_disable"); + return Component.translatable("neoforge.value.boolean.force_disable"); } else { - return Component.translatable("apothic_attributes.value.boolean.invalid"); + return Component.translatable("neoforge.value.boolean.invalid"); } } @Override public MutableComponent toComponent(AttributeModifier modif, TooltipFlag flag) { - Attribute attr = this.ths(); double value = modif.amount(); MutableComponent comp; - if (value > 0.0D) { - comp = Component.translatable("apothic_attributes.modifier.bool", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(ChatFormatting.BLUE); + comp = Component.translatable("neoforge.modifier.bool", this.toValueComponent(modif.operation(), value, flag), Component.translatable(this.getDescriptionId())).withStyle(ChatFormatting.BLUE); } else { value *= -1.0D; - comp = Component.translatable("apothic_attributes.modifier.bool", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(ChatFormatting.RED); + comp = Component.translatable("neoforge.modifier.bool", this.toValueComponent(modif.operation(), value, flag), Component.translatable(this.getDescriptionId())).withStyle(ChatFormatting.RED); } return comp.append(this.getDebugInfo(modif, flag)); diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 3c0ca35bb5..063b3f893f 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -194,7 +194,7 @@ public class NeoForgeMod { private static final DeferredHolder, SingletonArgumentInfo> MODID_COMMAND_ARGUMENT_TYPE = COMMAND_ARGUMENT_TYPES.register("modid", () -> ArgumentTypeInfos.registerByClass(ModIdArgument.class, SingletonArgumentInfo.contextFree(ModIdArgument::modIdArgument))); - public static final Holder SWIM_SPEED = ATTRIBUTES.register("swim_speed", () -> new RangedAttribute("neoforge.swim_speed", 1.0D, 0.0D, 1024.0D).setSyncable(true)); + public static final Holder SWIM_SPEED = ATTRIBUTES.register("swim_speed", () -> new PercentageAttribute("neoforge.swim_speed", 1.0D, 0.0D, 1024.0D).setSyncable(true)); public static final Holder NAMETAG_DISTANCE = ATTRIBUTES.register("nametag_distance", () -> new RangedAttribute("neoforge.name_tag_distance", 64.0D, 0.0D, 64.0).setSyncable(true)); /** @@ -471,6 +471,8 @@ public void setItemMovement(ItemEntity entity) { private static boolean enableProperFilenameValidation = false; private static boolean enableMilkFluid = false; + private static boolean enableMergedAttributeTooltips = false; + public static final DeferredHolder BUCKET_EMPTY_MILK = DeferredHolder.create(Registries.SOUND_EVENT, ResourceLocation.withDefaultNamespace("item.bucket.empty_milk")); public static final DeferredHolder BUCKET_FILL_MILK = DeferredHolder.create(Registries.SOUND_EVENT, ResourceLocation.withDefaultNamespace("item.bucket.fill_milk")); public static final DeferredHolder MILK_TYPE = DeferredHolder.create(NeoForgeRegistries.Keys.FLUID_TYPES, ResourceLocation.withDefaultNamespace("milk")); @@ -493,6 +495,13 @@ public static void enableMilkFluid() { enableMilkFluid = true; } + /** + * Run this during mod construction to enable merged attribute tooltip functionality. + */ + public static void enableMergedAttributeTooltips() { + enableMergedAttributeTooltips = true; + } + /** * Run this method during mod constructor to enable {@link net.minecraft.FileUtil#RESERVED_WINDOWS_FILENAMES_NEOFORGE} regex being used for filepath validation. * Fixes MC-268617 at cost of vanilla incompat edge cases with files generated with this activated and them migrated to vanilla instance - See PR #767 @@ -505,6 +514,10 @@ public static boolean getProperFilenameValidation() { return enableProperFilenameValidation; } + public static boolean shouldMergeAttributeTooltips() { + return enableMergedAttributeTooltips; + } + public NeoForgeMod(IEventBus modEventBus, Dist dist, ModContainer container) { LOGGER.info(NEOFORGEMOD, "NeoForge mod loading, version {}, for MC {}", NeoForgeVersion.getVersion(), DetectedVersion.BUILT_IN.getName()); ForgeSnapshotsMod.logStartupWarning(); diff --git a/src/main/java/net/neoforged/neoforge/common/PercentageAttribute.java b/src/main/java/net/neoforged/neoforge/common/PercentageAttribute.java new file mode 100644 index 0000000000..426ff68029 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/PercentageAttribute.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation; +import net.minecraft.world.entity.ai.attributes.RangedAttribute; +import net.minecraft.world.item.TooltipFlag; + +/** + * A Percentage Attribute is one which always displays modifiers as percentages, including for {@link Operation#ADD_VALUE}. + *

+ * This is used for attributes that would not make sense being displayed as flat additions (ex: +0.05 Swim Speed). + */ +public class PercentageAttribute extends RangedAttribute { + public PercentageAttribute(String pDescriptionId, double pDefaultValue, double pMin, double pMax) { + super(pDescriptionId, pDefaultValue, pMin, pMax); + } + + @Override + public MutableComponent toValueComponent(Operation op, double value, TooltipFlag flag) { + return Component.translatable("neoforge.value.percent", FORMAT.format(value * 100)); + } +} diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IAttributeExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IAttributeExtension.java index cba5ecb922..45ee1f524a 100644 --- a/src/main/java/net/neoforged/neoforge/common/extensions/IAttributeExtension.java +++ b/src/main/java/net/neoforged/neoforge/common/extensions/IAttributeExtension.java @@ -14,8 +14,10 @@ import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.TextColor; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.Attribute.Sentiment; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation; import net.minecraft.world.entity.ai.attributes.Attributes; @@ -23,7 +25,7 @@ import net.minecraft.world.item.TooltipFlag; import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.util.AttributeUtil; -import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; public interface IAttributeExtension { @@ -41,19 +43,21 @@ public interface IAttributeExtension { * @return The component form of the formatted value. */ default MutableComponent toValueComponent(@Nullable Operation op, double value, TooltipFlag flag) { - // Knockback Resistance and Swim Speed are percent-based attributes, but we can't registry replace attributes, so we do this here. + // Knockback Resistance should be a PercentageAttribute, but we can't registry replace attributes, so we do this here. // For Knockback Resistance, vanilla hardcodes a multiplier of 10 for addition values to hide numbers lower than 1, - // but percent-based is the real desire. - // For Swim Speed, the implementation is percent-based, but no additional tricks are performed. - if (this == Attributes.KNOCKBACK_RESISTANCE || this == NeoForgeMod.SWIM_SPEED.value()) { - return Component.translatable("apothic_attributes.value.percent", FORMAT.format(value * 100)); + // but representing the value as a percentage better expresses the intent of the modifier. + if (this == Attributes.KNOCKBACK_RESISTANCE.value()) { + return Component.translatable("neoforge.value.percent", FORMAT.format(value * 100)); } + // Speed has no metric, so displaying everything as percent works better for the user. // However, Speed also operates in that the default is 0.1, not 1, so we have to special-case it instead of including it above. - if (this == Attributes.MOVEMENT_SPEED && isNullOrAddition(op)) { - return Component.translatable("apothic_attributes.value.percent", FORMAT.format(value * 1000)); + if (this == Attributes.MOVEMENT_SPEED.value() && isNullOrAddition(op)) { + return Component.translatable("neoforge.value.percent", FORMAT.format(value * 1000)); } - String key = isNullOrAddition(op) ? "apothic_attributes.value.flat" : "apothic_attributes.value.percent"; + + // Default case for "normal" ranged attributes. + String key = isNullOrAddition(op) ? "neoforge.value.flat" : "neoforge.value.percent"; return Component.translatable(key, FORMAT.format(isNullOrAddition(op) ? value : value * 100)); } @@ -69,16 +73,16 @@ default MutableComponent toValueComponent(@Nullable Operation op, double value, * @return The component representation of the passed attribute modifier, with debug info appended if enabled. */ default MutableComponent toComponent(AttributeModifier modif, TooltipFlag flag) { - Attribute attr = this.ths(); + Attribute attr = self(); double value = modif.amount(); MutableComponent comp; if (value > 0.0D) { - comp = Component.translatable("apothic_attributes.modifier.plus", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(attr.getStyle(true)); + comp = Component.translatable("neoforge.modifier.plus", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(attr.getStyle(true)); } else { value *= -1.0D; - comp = Component.translatable("apothic_attributes.modifier.take", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(attr.getStyle(false)); + comp = Component.translatable("neoforge.modifier.take", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(attr.getStyle(false)); } return comp.append(this.getDebugInfo(modif, flag)); @@ -104,8 +108,7 @@ default Component getDebugInfo(AttributeModifier modif, TooltipFlag flag) { case ADD_MULTIPLIED_BASE -> advValue > 0 ? String.format("[+%sx]", valueStr) : String.format("[%sx]", valueStr); case ADD_MULTIPLIED_TOTAL -> String.format("[x%s]", valueStr); }; - debugInfo = Component.literal(" ") - .append(Component.literal(txt).withStyle(ChatFormatting.GRAY)); + debugInfo = Component.literal(" ").append(Component.literal(txt).withStyle(ChatFormatting.GRAY)); } return debugInfo; } @@ -118,7 +121,7 @@ default Component getDebugInfo(AttributeModifier modif, TooltipFlag flag) { * @return The UUID of the "base" modifier, or null, if no such modifier may exist. */ @Nullable - default ResourceLocation getBaseID() { + default ResourceLocation getBaseId() { if (this == Attributes.ATTACK_DAMAGE.value()) return AttributeUtil.BASE_ATTACK_DAMAGE_ID; else if (this == Attributes.ATTACK_SPEED.value()) return AttributeUtil.BASE_ATTACK_SPEED_ID; else if (this == Attributes.ENTITY_INTERACTION_RANGE.value()) return AttributeUtil.BASE_ENTITY_REACH_ID; @@ -136,15 +139,14 @@ default ResourceLocation getBaseID() { * @return The component representation of the passed attribute modifier. */ default MutableComponent toBaseComponent(double value, double entityBase, boolean merged, TooltipFlag flag) { - Attribute attr = this.ths(); + Attribute attr = self(); Component debugInfo = CommonComponents.EMPTY; if (flag.isAdvanced() && !merged) { // Advanced Tooltips cause us to emit the entity's base value and the base value of the item. debugInfo = Component.literal(" ") - .append( - Component.translatable(NeoForgeVersion.MOD_ID + ".adv.base", FORMAT.format(entityBase), FORMAT.format(value - entityBase)).withStyle(ChatFormatting.GRAY)); + .append(Component.translatable("neoforge.adv.base", FORMAT.format(entityBase), FORMAT.format(value - entityBase)).withStyle(ChatFormatting.GRAY)); } MutableComponent comp = Component.translatable("attribute.modifier.equals.0", FORMAT.format(value), Component.translatable(attr.getDescriptionId())); @@ -152,6 +154,16 @@ default MutableComponent toBaseComponent(double value, double entityBase, boolea return comp.append(debugInfo); } + /** + * Returns the color used by merged attribute modifiers. Only used when {@link NeoForgeMod#enableMergedAttributeTooltips()} is active. + *

+ * Similarly to {@link Attribute#getStyle(boolean)}, this method should return a color based on the attribute's {@link Sentiment}. + * The returned color should be distinguishable from the color used by {@link Attribute#getStyle(boolean)}. + * + * @param positive If the attribute modifier value is positive or not. + */ + TextColor getMergedStyle(boolean isPositive); + /** * Certain attributes, such as Attack Damage, are increased by an Enchantment that doesn't actually apply * an attribute modifier.
@@ -159,7 +171,10 @@ default MutableComponent toBaseComponent(double value, double entityBase, boolea * * @param stack The stack in question. * @return Any bonus value to be applied to the attribute's value, after all modifiers have been applied. + * + * @apiNote This method is experimental as it only exists to support displaying Sharpness in item tooltips, which is currently broken by MC-271840 */ + @ApiStatus.Experimental default double getBonusBaseValue(ItemStack stack) { // if (this == Attributes.ATTACK_DAMAGE) return EnchantmentHelper.getDamageBonus(stack, MobType.UNDEFINED); return 0; @@ -172,14 +187,17 @@ default double getBonusBaseValue(ItemStack stack) { * @param stack The stack in question. * @param tooltip The tooltip consumer. * @param flag The tooltip flag. + * + * @apiNote This method is experimental as it only exists to support displaying Sharpness in item tooltips, which is currently broken by MC-271840 */ + @ApiStatus.Experimental default void addBonusTooltips(ItemStack stack, Consumer tooltip, TooltipFlag flag) { // if (this == Attributes.ATTACK_DAMAGE) { // float sharpness = EnchantmentHelper.getDamageBonus(stack, MobType.UNDEFINED); // Component debugInfo = CommonComponents.EMPTY; // if (flag.isAdvanced()) { // // Show the user that this fake modifier is from Sharpness. - // debugInfo = Component.literal(" ").append(Component.translatable(ApothicAttributes.MODID + ".adv.sharpness_bonus", + // debugInfo = Component.literal(" ").append(Component.translatable("neoforge.adv.sharpness_bonus", // sharpness).withStyle(ChatFormatting.GRAY)); // } // MutableComponent comp = AttributeHelper.list() @@ -189,11 +207,11 @@ default void addBonusTooltips(ItemStack stack, Consumer tooltip, Tool // } } - default Attribute ths() { - return (Attribute) this; + public static boolean isNullOrAddition(@Nullable Operation op) { + return op == null || op == Operation.ADD_VALUE; } - static boolean isNullOrAddition(@Nullable Operation op) { - return op == null || op == Operation.ADD_VALUE; + private Attribute self() { + return (Attribute) this; } } diff --git a/src/main/java/net/neoforged/neoforge/common/util/AttributeUtil.java b/src/main/java/net/neoforged/neoforge/common/util/AttributeUtil.java index 70a3eb59a2..5ffca0831a 100644 --- a/src/main/java/net/neoforged/neoforge/common/util/AttributeUtil.java +++ b/src/main/java/net/neoforged/neoforge/common/util/AttributeUtil.java @@ -16,9 +16,16 @@ import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.common.extensions.IAttributeExtension; +import net.neoforged.neoforge.event.ItemAttributeModifierEvent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +/** + * Utility code to support {@link IAttributeExtension}. + * + * @see {@link TooltipUtil} for additional tooltip-related code + */ public class AttributeUtil { /** * UUID of the base modifier for Attack Damage @@ -39,7 +46,7 @@ public class AttributeUtil { * Comparator for {@link AttributeModifier}. First compares by operation, then amount, then the ID. */ public static final Comparator ATTRIBUTE_MODIFIER_COMPARATOR = Comparator.comparing(AttributeModifier::operation) - .thenComparing(Comparator.comparingDouble(AttributeModifier::amount)) + .thenComparing(Comparator.comparingDouble(a -> -Math.abs(a.amount()))) // Sort most impactful modifiers first .thenComparing(Comparator.comparing(AttributeModifier::id)); private static final Logger LOGGER = LogManager.getLogger(); @@ -52,10 +59,12 @@ public static Multimap, AttributeModifier> sortedMap() { } /** + * Returns a sorted, mutable {@link Multimap} containing all the attribute modifiers on an item stack for the given group. + *

+ * This includes attribute modifiers from components (or default modifiers, if not present), enchantments, and the {@link ItemAttributeModifierEvent}. * - * @param stack - * @param slot - * @return + * @param stack The stack to query modifiers for. + * @param slot The slot group to query modifiers for. */ public static Multimap, AttributeModifier> getSortedModifiers(ItemStack stack, EquipmentSlotGroup slot) { Multimap, AttributeModifier> map = AttributeUtil.sortedMap(); diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 0d78311974..d2b5b06ca3 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -263,5 +263,21 @@ "neoforge.network.extensible_enums.no_vanilla_server": "This client does not support vanilla servers as it has extended enums used in serverbound networking", "neoforge.network.extensible_enums.enum_set_mismatch": "The set of extensible enums on the client and server do not match. Make sure you are using the same NeoForge version as the server", - "neoforge.network.extensible_enums.enum_entry_mismatch": "The set of values added to extensible enums on the client and server do not match. Make sure you are using the same mod and NeoForge versions as the server. See the log for more details" + "neoforge.network.extensible_enums.enum_entry_mismatch": "The set of values added to extensible enums on the client and server do not match. Make sure you are using the same mod and NeoForge versions as the server. See the log for more details", + + "neoforge.adv.base": "[Entity: %s | Item: %s]", + "neoforge.adv.sharpness_bonus": "[Sharp: +%s]", + + "neoforge.value.flat": "%s", + "neoforge.value.percent": "%s%%", + + "neoforge.value.boolean.enabled": "Enabled", + "neoforge.value.boolean.disabled": "Disabled", + "neoforge.value.boolean.enable": "Enables", + "neoforge.value.boolean.force_disable": "Forcibly Disables", + "neoforge.value.boolean.invalid": "Invalid", + + "neoforge.modifier.plus": "+%s %s", + "neoforge.modifier.take": "-%s %s", + "neoforge.modifier.bool": "%s %s" }