-
-
Notifications
You must be signed in to change notification settings - Fork 166
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
Shadows-of-Fire
wants to merge
9
commits into
neoforged:1.21.x
Choose a base branch
from
Shadows-of-Fire:attr-tooltips
base: 1.21.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+956
−4
Open
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
90c20b0
Initial IFormattableAttribute implementation
Shadows-of-Fire 610bee9
Finish API-ifying the implementation
Shadows-of-Fire c1be879
Fix sentiment on nametag distance
Shadows-of-Fire f5e58c2
Make potion tooltips use new extensions
Shadows-of-Fire 0e265f9
Switch to TooltipFlag key modifier methods
Shadows-of-Fire 8990ed5
Move group name logic to applyModifierTooltips
Shadows-of-Fire 82321aa
Flattern TooltipUtil into AttributeUtil
Shadows-of-Fire 2c95e34
update event sidedness docs
Shadows-of-Fire e9e5871
Add the ability to skip entire groups to GatherSkipped
Shadows-of-Fire File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
patches/net/minecraft/world/entity/ai/attributes/Attribute.java.patch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
src/main/java/net/neoforged/neoforge/client/event/AddAttributeTooltipsEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
src/main/java/net/neoforged/neoforge/client/event/GatherSkippedAttributeTooltipsEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
171
src/main/java/net/neoforged/neoforge/client/util/TooltipUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure