From fa5fe9b64407f4dedfa2d37b833ec60ff567f29d Mon Sep 17 00:00:00 2001
From: Tas <103238549+0xTas@users.noreply.github.com>
Date: Tue, 8 Oct 2024 18:41:02 -0700
Subject: [PATCH] Fix BetterTooltips crashing over items with the HIDE_TOOLTIP
 flag (#4913)

---
 .../events/game/ItemStackTooltipEvent.java    | 48 +++++++++++++++++-
 .../modules/render/BetterTooltips.java        | 50 ++++++++++++-------
 2 files changed, 79 insertions(+), 19 deletions(-)

diff --git a/src/main/java/meteordevelopment/meteorclient/events/game/ItemStackTooltipEvent.java b/src/main/java/meteordevelopment/meteorclient/events/game/ItemStackTooltipEvent.java
index c0cf9c5f97..9a5bbf8d21 100644
--- a/src/main/java/meteordevelopment/meteorclient/events/game/ItemStackTooltipEvent.java
+++ b/src/main/java/meteordevelopment/meteorclient/events/game/ItemStackTooltipEvent.java
@@ -5,9 +5,55 @@
 
 package meteordevelopment.meteorclient.events.game;
 
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
 import net.minecraft.item.ItemStack;
 import net.minecraft.text.Text;
 
 import java.util.List;
 
-public record ItemStackTooltipEvent(ItemStack itemStack, List<Text> list) {}
+public class ItemStackTooltipEvent {
+    private final ItemStack itemStack;
+    private List<Text> list;
+
+    public ItemStackTooltipEvent(ItemStack itemStack, List<Text> list) {
+        this.itemStack = itemStack;
+        this.list = list;
+    }
+
+    public List<Text> list() {
+        return list;
+    }
+
+    public ItemStack itemStack() {
+        return itemStack;
+    }
+
+    public void appendStart(Text text) {
+        copyIfImmutable();
+        int index = list.isEmpty() ? 0 : 1;
+        list.add(index, text);
+    }
+
+    public void appendEnd(Text text) {
+        copyIfImmutable();
+        list.add(text);
+    }
+
+    public void append(int index, Text text) {
+        copyIfImmutable();
+        list.add(index, text);
+    }
+
+    public void set(int index, Text text) {
+        copyIfImmutable();
+        list.set(index, text);
+    }
+
+    private void copyIfImmutable() {
+        // ItemStack#getTooltip can sometimes return List.of(), which is immutable.
+        // Some modules like BetterTooltips try to modify that list anyway, which causes a crash if we don't replace it.
+        if (List.of().getClass().getSuperclass().isInstance(list)) {
+            list = new ObjectArrayList<>(list);
+        }
+    }
+}
diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java
index 9336b51568..64a8c345d7 100644
--- a/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java
+++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/render/BetterTooltips.java
@@ -253,6 +253,13 @@ public BetterTooltips() {
 
     @EventHandler
     private void appendTooltip(ItemStackTooltipEvent event) {
+        // Hide hidden (empty) tooltips unless the tooltip hide flag setting is true.
+        if (!tooltip.get() && event.list().isEmpty()) {
+            // Hold-to-preview tooltip text is always added when needed.
+            appendPreviewTooltipText(event, false);
+            return;
+        }
+
         // Status effects
         if (statusEffects.get()) {
             if (event.itemStack().getItem() == Items.SUSPICIOUS_STEW) {
@@ -260,13 +267,13 @@ private void appendTooltip(ItemStackTooltipEvent event) {
                 if (stewEffectsComponent != null) {
                     for (StewEffect effectTag : stewEffectsComponent.effects()) {
                         StatusEffectInstance effect = new StatusEffectInstance(effectTag.effect(), effectTag.duration(), 0);
-                        event.list().add(1, getStatusText(effect));
+                        event.appendStart(getStatusText(effect));
                     }
                 }
             } else {
                 FoodComponent food = event.itemStack().get(DataComponentTypes.FOOD);
                 if (food != null) {
-                    food.effects().forEach(e -> event.list().add(1, getStatusText(e.effect())));
+                    food.effects().forEach(e -> event.appendStart(getStatusText(e.effect())));
                 }
             }
         }
@@ -277,12 +284,12 @@ private void appendTooltip(ItemStackTooltipEvent event) {
                 BlockStateComponent blockStateComponent = event.itemStack().get(DataComponentTypes.BLOCK_STATE);
                 if (blockStateComponent != null) {
                     String level = blockStateComponent.properties().get("honey_level");
-                    event.list().add(1, Text.literal(String.format("%sHoney level: %s%s%s.", Formatting.GRAY, Formatting.YELLOW, level, Formatting.GRAY)));
+                    event.appendStart(Text.literal(String.format("%sHoney level: %s%s%s.", Formatting.GRAY, Formatting.YELLOW, level, Formatting.GRAY)));
                 }
 
                 List<BeehiveBlockEntity.BeeData> bees = event.itemStack().get(DataComponentTypes.BEES);
                 if (bees != null) {
-                    event.list().add(1, Text.literal(String.format("%sBees: %s%d%s.", Formatting.GRAY, Formatting.YELLOW, bees.size(), Formatting.GRAY)));
+                    event.appendStart(Text.literal(String.format("%sBees: %s%d%s.", Formatting.GRAY, Formatting.YELLOW, bees.size(), Formatting.GRAY)));
                 }
             }
         }
@@ -300,25 +307,14 @@ private void appendTooltip(ItemStackTooltipEvent event) {
                 if (byteCount >= 1024) count = String.format("%.2f kb", byteCount / (float) 1024);
                 else count = String.format("%d bytes", byteCount);
 
-                event.list().add(Text.literal(count).formatted(Formatting.GRAY));
+                event.appendEnd(Text.literal(count).formatted(Formatting.GRAY));
             } catch (Exception e) {
-                event.list().add(Text.literal("Error getting bytes.").formatted(Formatting.RED));
+                event.appendEnd(Text.literal("Error getting bytes.").formatted(Formatting.RED));
             }
         }
 
         // Hold to preview tooltip
-        if ((shulkers.get() && !previewShulkers() && Utils.hasItems(event.itemStack()))
-            || (event.itemStack().getItem() == Items.ENDER_CHEST && echest.get() && !previewEChest())
-            || (event.itemStack().getItem() == Items.FILLED_MAP && maps.get() && !previewMaps())
-            || (event.itemStack().getItem() == Items.WRITABLE_BOOK && books.get() && !previewBooks())
-            || (event.itemStack().getItem() == Items.WRITTEN_BOOK && books.get() && !previewBooks())
-            || (event.itemStack().getItem() instanceof EntityBucketItem && entitiesInBuckets.get() && !previewEntities())
-            || (event.itemStack().getItem() instanceof BannerItem && banners.get() && !previewBanners())
-            || (event.itemStack().getItem() instanceof BannerPatternItem && banners.get() && !previewBanners())
-            || (event.itemStack().getItem() == Items.SHIELD && banners.get() && !previewBanners())) {
-            event.list().add(Text.literal(""));
-            event.list().add(Text.literal("Hold " + Formatting.YELLOW + keybind + Formatting.RESET + " to preview"));
-        }
+        appendPreviewTooltipText(event, true);
     }
 
     @EventHandler
@@ -408,6 +404,24 @@ public void applyCompactShulkerTooltip(ItemStack shulkerItem, List<Text> tooltip
         }
     }
 
+    private void appendPreviewTooltipText(ItemStackTooltipEvent event, boolean spacer) {
+        if (!isPressed() && (
+            shulkers.get() && Utils.hasItems(event.itemStack())
+            || (event.itemStack().getItem() == Items.ENDER_CHEST && echest.get())
+            || (event.itemStack().getItem() == Items.FILLED_MAP && maps.get())
+            || (event.itemStack().getItem() == Items.WRITABLE_BOOK && books.get())
+            || (event.itemStack().getItem() == Items.WRITTEN_BOOK && books.get())
+            || (event.itemStack().getItem() instanceof EntityBucketItem && entitiesInBuckets.get())
+            || (event.itemStack().getItem() instanceof BannerItem && banners.get())
+            || (event.itemStack().getItem() instanceof BannerPatternItem && banners.get())
+            || (event.itemStack().getItem() == Items.SHIELD && banners.get())
+        )) {
+            // we don't want to add the spacer if the tooltip is hidden
+            if (spacer) event.appendEnd(Text.literal(""));
+            event.appendEnd(Text.literal("Hold " + Formatting.YELLOW + keybind + Formatting.RESET + " to preview"));
+        }
+    }
+
     private MutableText getStatusText(StatusEffectInstance effect) {
         MutableText text = Text.translatable(effect.getTranslationKey());
         if (effect.getAmplifier() != 0) {