Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.21] Add an Attribute formatting API for better control of attribute tooltips #1551

Open
wants to merge 9 commits into
base: 1.21.x
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- a/net/minecraft/world/entity/ai/attributes/Attribute.java
+++ b/net/minecraft/world/entity/ai/attributes/Attribute.java
@@ -9,7 +_,7 @@
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;

-public class Attribute {
+public class Attribute implements net.neoforged.neoforge.common.extensions.IAttributeExtension {
public static final Codec<Holder<Attribute>> CODEC = BuiltInRegistries.ATTRIBUTE.holderByNameCodec();
public static final StreamCodec<RegistryFriendlyByteBuf, Holder<Attribute>> STREAM_CODEC = ByteBufCodecs.holderRegistry(Registries.ATTRIBUTE);
private final double defaultValue;
16 changes: 15 additions & 1 deletion patches/net/minecraft/world/item/ItemStack.java.patch
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,17 @@
if (this.has(DataComponents.CUSTOM_NAME)) {
mutablecomponent.withStyle(ChatFormatting.ITALIC);
}
@@ -784,12 +_,14 @@
@@ -752,7 +_,8 @@
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_);
this.addToTooltip(DataComponents.UNBREAKABLE, p_339637_, consumer, p_41653_);
AdventureModePredicate adventuremodepredicate = this.get(DataComponents.CAN_BREAK);
if (adventuremodepredicate != null && adventuremodepredicate.showInTooltip()) {
@@ -784,12 +_,18 @@
list.add(DISABLED_ITEM_TOOLTIP);
}

Expand All @@ -157,6 +167,10 @@
}
}

+ /**
+ * @deprecated Neo: Use {@link net.neoforged.neoforge.client.util.TooltipUtil#addAttributeTooltips}
+ */
+ @Deprecated
private void addAttributeTooltips(Consumer<Component> p_330796_, @Nullable Player p_330530_) {
ItemAttributeModifiers itemattributemodifiers = this.getOrDefault(DataComponents.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.EMPTY);
+ // Neo: We don't need to call IItemStackExtension#getAttributeModifiers here, since it will be done in forEachModifier.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we are deprecating this method now, should we just remove the comment inside it that we are patching in as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.event;

import java.util.function.Consumer;
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.neoforged.api.distmarker.Dist;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import org.jetbrains.annotations.Nullable;

/**
* This event is used to add additional attribute tooltip lines without having to manually locate the inject point.
* <p>
* This event is only fired on the {@linkplain Dist#CLIENT physical client}.
*/
public class AddAttributeTooltipsEvent extends PlayerEvent {
protected final ItemStack stack;
protected final Consumer<Component> tooltip;
protected final TooltipFlag flag;

public AddAttributeTooltipsEvent(ItemStack stack, @Nullable Player player, Consumer<Component> tooltip, TooltipFlag flag) {
super(player);
this.stack = stack;
this.tooltip = tooltip;
this.flag = flag;
}

/**
* Use to determine if the advanced information on item tooltips is being shown, toggled by F3+H.
*/
public TooltipFlag getFlags() {
return this.flag;
}

/**
* The {@link ItemStack} with the tooltip.
*/
public ItemStack getStack() {
return this.stack;
}

/**
* Adds a single {@link Component} to the itemstack's tooltip.
*/
public void addTooltipLine(Component comp) {
this.tooltip.accept(comp);
}

/**
* This event is fired with a null player during startup when populating search trees for tooltips.
*/
@Override
@Nullable
public Player getEntity() {
return super.getEntity();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.event;

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;

/**
* This event is used to collect UUIDs of attribute modifiers that will not be displayed in item tooltips.
* <p>
* This allows hiding specific modifiers for whatever reason. They will still be shown in the attributes GUI.
* <p>
* This event is only fired on the {@linkplain Dist#CLIENT physical client}.
*/
public class GatherSkippedAttributeTooltipsEvent extends PlayerEvent {
protected final ItemStack stack;
protected final Set<ResourceLocation> skips;
protected final TooltipFlag flag;

public GatherSkippedAttributeTooltipsEvent(ItemStack stack, @Nullable Player player, Set<ResourceLocation> skips, TooltipFlag flag) {
super(player);
this.stack = stack;
this.skips = skips;
this.flag = flag;
}

/**
* Use to determine if the advanced information on item tooltips is being shown, toggled by F3+H.
*/
public TooltipFlag getFlags() {
return this.flag;
}

/**
* The {@link ItemStack} with the tooltip.
*/
public ItemStack getStack() {
return this.stack;
}

/**
* Mark the ResourceLocation of a specific attribute modifier as skipped, causing it to not be displayed in the tooltip.
*/
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.
*/
@Override
@Nullable
public Player getEntity() {
return super.getEntity();
}
}
171 changes: 171 additions & 0 deletions src/main/java/net/neoforged/neoforge/client/util/TooltipUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.util;
Shadows-of-Fire marked this conversation as resolved.
Show resolved Hide resolved

import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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.component.DataComponents;
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.EquipmentSlotGroup;
import net.minecraft.world.entity.ai.attributes.Attribute;
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.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.ItemAttributeModifiers;
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.util.AttributeUtil;
import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion;
import org.jetbrains.annotations.Nullable;

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<Component> tooltip, TooltipFlag flag) {
ItemAttributeModifiers modifiers = stack.getOrDefault(DataComponents.ATTRIBUTE_MODIFIERS, ItemAttributeModifiers.EMPTY);
if (modifiers.showInTooltip()) {
applyModifierTooltips(player, stack, tooltip, flag);
}
NeoForge.EVENT_BUS.post(new AddAttributeTooltipsEvent(stack, player, tooltip, flag));
}

public static void applyModifierTooltips(@Nullable Player player, ItemStack stack, Consumer<Component> tooltip, TooltipFlag flag) {
Set<ResourceLocation> skips = new HashSet<>();
Shadows-of-Fire marked this conversation as resolved.
Show resolved Hide resolved
NeoForge.EVENT_BUS.post(new GatherSkippedAttributeTooltipsEvent(stack, player, skips, flag));

for (EquipmentSlotGroup group : EquipmentSlotGroup.values()) {
Multimap<Holder<Attribute>, AttributeModifier> modifiers = AttributeUtil.getSortedModifiers(stack, group);
applyTextFor(player, stack, tooltip, modifiers, group, skips, flag);
}
}

public static void applyTextFor(@Nullable Player player, ItemStack stack, Consumer<Component> tooltip, Multimap<Holder<Attribute>, AttributeModifier> modifierMap,
EquipmentSlotGroup group, Set<ResourceLocation> skips, TooltipFlag flag) {
if (!modifierMap.isEmpty()) {
modifierMap.values().removeIf(m -> skips.contains(m.id()));

tooltip.accept(Component.empty());
tooltip.accept(Component.translatable("item.modifiers." + group.getSerializedName()).withStyle(ChatFormatting.GRAY));

if (modifierMap.isEmpty()) return;

Map<Holder<Attribute>, BaseModifier> baseModifs = new IdentityHashMap<>();

modifierMap.forEach((attr, modif) -> {
if (modif.id().equals(attr.value().getBaseID())) {
baseModifs.put(attr, new BaseModifier(modif, new ArrayList<>()));
}
});

modifierMap.forEach((attr, modif) -> {
BaseModifier base = baseModifs.get(attr);
if (base != null && base.base != modif) {
base.children.add(modif);
}
});

for (Map.Entry<Holder<Attribute>, BaseModifier> entry : baseModifs.entrySet()) {
Holder<Attribute> 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);
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);
}
}
}

for (Holder<Attribute> attr : modifierMap.keySet()) {
if (baseModifs.containsKey(attr)) continue;
Collection<AttributeModifier> modifs = modifierMap.get(attr);
// Initiate merged-tooltip logic if we have more than one modifier for a given attribute.
Shadows-of-Fire marked this conversation as resolved.
Show resolved Hide resolved
if (modifs.size() > 1) {
double[] sums = new double[3];
boolean[] merged = new boolean[3];
Map<Operation, List<AttributeModifier>> 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 (sums[i] == 0) continue;
if (merged[i]) {
TextColor color = sums[i] < 0 ? TextColor.fromRgb(0xF93131) : TextColor.fromRgb(0x7A7AF9);
Shadows-of-Fire marked this conversation as resolved.
Show resolved Hide resolved
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));
}
}
} else modifs.forEach(m -> {
if (m.amount() != 0) tooltip.accept(attr.value().toComponent(m, flag));
});
}
}
}

/**
* Creates a mutable component starting with the char used to represent a drop-down list.
*/
public static MutableComponent list() {
return Component.literal(" \u2507 ").withStyle(ChatFormatting.GRAY);
}

/**
* Creates a literal component holding {@code padding} and appends {@code comp} to it, returning the result.
*/
private static MutableComponent padded(String padding, Component comp) {
return Component.literal(padding).append(comp);
}

public static record BaseModifier(AttributeModifier base, List<AttributeModifier> children) {}
}
36 changes: 36 additions & 0 deletions src/main/java/net/neoforged/neoforge/common/BooleanAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@

package net.neoforged.neoforge.common;

import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.AttributeModifier.Operation;
import net.minecraft.world.item.TooltipFlag;
import org.jetbrains.annotations.Nullable;

/**
* A boolean attribute only has two states, on or off, represented by a value of 0 (false) or 1 (true).
Expand Down Expand Up @@ -34,4 +40,34 @@ public double sanitizeValue(double value) {
}
return value > 0 ? 1 : 0;
}

@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"));
} else if (op == Operation.ADD_VALUE && value > 0) {
return Component.translatable("apothic_attributes.value.boolean.enable");
} else if (op == Operation.ADD_MULTIPLIED_TOTAL && (int) value == -1) {
return Component.translatable("apothic_attributes.value.boolean.force_disable");
} else {
return Component.translatable("apothic_attributes.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);
} else {
value *= -1.0D;
comp = Component.translatable("apothic_attributes.modifier.bool", this.toValueComponent(modif.operation(), value, flag), Component.translatable(attr.getDescriptionId())).withStyle(ChatFormatting.RED);
}

return comp.append(this.getDebugInfo(modif, flag));
}
}
Loading