aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVixid <52578495+VixidDev@users.noreply.github.com>2024-04-25 08:43:01 +0100
committerGitHub <noreply@github.com>2024-04-25 09:43:01 +0200
commitf3b3b44296b8a6ac77ecad433ca28e7a03a9eaf2 (patch)
tree4a1e45727434c16489c748fad7eefe4117be5875
parented53f750e6b68067f58a5e706db220760d57029f (diff)
downloadskyhanni-f3b3b44296b8a6ac77ecad433ca28e7a03a9eaf2.tar.gz
skyhanni-f3b3b44296b8a6ac77ecad433ca28e7a03a9eaf2.tar.bz2
skyhanni-f3b3b44296b8a6ac77ecad433ca28e7a03a9eaf2.zip
Feature: SBA style Enchant Parsing (#654)
Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com>
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/inventory/EnchantParsingConfig.java93
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/ChatHoverEvent.kt17
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Cache.kt23
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Enchant.kt77
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantParser.kt355
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantsJson.kt41
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/FormattedEnchant.kt26
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiChatHook.kt40
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinGuiChat.java22
11 files changed, 700 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
index cdaf008a1..45055829c 100644
--- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
+++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
@@ -342,6 +342,7 @@ import at.hannibal2.skyhanni.features.misc.items.AuctionHouseCopyUnderbidPrice
import at.hannibal2.skyhanni.features.misc.items.EstimatedItemValue
import at.hannibal2.skyhanni.features.misc.items.EstimatedWardrobePrice
import at.hannibal2.skyhanni.features.misc.items.GlowingDroppedItems
+import at.hannibal2.skyhanni.features.misc.items.enchants.EnchantParser
import at.hannibal2.skyhanni.features.misc.limbo.LimboPlaytime
import at.hannibal2.skyhanni.features.misc.limbo.LimboTimeTracker
import at.hannibal2.skyhanni.features.misc.massconfiguration.DefaultConfigFeatures
@@ -842,6 +843,7 @@ class SkyHanniMod {
loadModule(DungeonFinderFeatures())
loadModule(GoldenGoblinHighlight())
loadModule(TabWidgetSettings())
+ loadModule(EnchantParser)
loadModule(PabloHelper())
loadModule(FishingBaitWarnings())
loadModule(CustomScoreboard())
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/EnchantParsingConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/EnchantParsingConfig.java
new file mode 100644
index 000000000..bada2edee
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/EnchantParsingConfig.java
@@ -0,0 +1,93 @@
+package at.hannibal2.skyhanni.config.features.inventory;
+
+import at.hannibal2.skyhanni.config.FeatureToggle;
+import at.hannibal2.skyhanni.utils.LorenzColor;
+import com.google.gson.annotations.Expose;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigOption;
+import io.github.notenoughupdates.moulconfig.observer.Property;
+
+public class EnchantParsingConfig {
+
+ @Expose
+ @ConfigOption(name = "Enable", desc = "Toggle for coloring the enchants. Turn this off if you want to use enchant parsing from other mods.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public Property<Boolean> colorParsing = Property.of(true);
+
+ @Expose
+ @ConfigOption(name = "Format", desc = "The way the enchants are formatted in the tooltip.")
+ @ConfigEditorDropdown()
+ public Property<EnchantFormat> format = Property.of(EnchantFormat.NORMAL);
+
+ public enum EnchantFormat {
+ NORMAL("Normal"),
+ COMPRESSED("Compressed"),
+ STACKED("Stacked");
+
+ public final String str;
+
+ EnchantFormat(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public String toString() {
+ return str;
+ }
+ }
+
+ @Expose
+ @ConfigOption(name = "Perfect Enchantment Color", desc = "The color an enchantment will be at max level.")
+ @ConfigEditorDropdown()
+ public Property<LorenzColor> perfectEnchantColor = Property.of(LorenzColor.CHROMA);
+
+ @Expose
+ @ConfigOption(name = "Great Enchantment Color", desc = "The color an enchantment will be at a great level.")
+ @ConfigEditorDropdown()
+ public Property<LorenzColor> greatEnchantColor = Property.of(LorenzColor.GOLD);
+
+ @Expose
+ @ConfigOption(name = "Good Enchantment Color", desc = "The color an enchantment will be at a good level.")
+ @ConfigEditorDropdown()
+ public Property<LorenzColor> goodEnchantColor = Property.of(LorenzColor.BLUE);
+
+ @Expose
+ @ConfigOption(name = "Poor Enchantment Color", desc = "The color an enchantment will be at a poor level.")
+ @ConfigEditorDropdown()
+ public Property<LorenzColor> poorEnchantColor = Property.of(LorenzColor.GRAY);
+
+ @Expose
+ @ConfigOption(name = "Comma Format", desc = "Change the format of the comma after each enchant.")
+ @ConfigEditorDropdown()
+ public Property<CommaFormat> commaFormat = Property.of(CommaFormat.COPY_ENCHANT);
+
+ public enum CommaFormat {
+ COPY_ENCHANT("Copy enchant format"),
+ DEFAULT("Default (Blue)");
+
+ public final String str;
+
+ CommaFormat(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public String toString() {
+ return str;
+ }
+ }
+
+ @Expose
+ @ConfigOption(name = "Hide Vanilla Enchants", desc = "Hide the regular vanilla enchants usually found in the first 1-2 lines of lore.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public Property<Boolean> hideVanillaEnchants = Property.of(true);
+
+ @Expose
+ @ConfigOption(name = "Hide Enchant Description", desc = "Hides the enchant description after each enchant if available.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public Property<Boolean> hideEnchantDescriptions = Property.of(false);
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java
index 1bef14366..518730117 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java
@@ -37,6 +37,10 @@ public class InventoryConfig {
public BazaarConfig bazaar = new BazaarConfig();
@Expose
+ @Category(name = "Enchant Parsing", desc = "Settings for Skyhanni's Enchant Parsing")
+ public EnchantParsingConfig enchantParsing = new EnchantParsingConfig();
+
+ @Expose
@Category(name = "Helpers", desc = "Settings for Helpers")
public HelperConfig helper = new HelperConfig();
diff --git a/src/main/java/at/hannibal2/skyhanni/events/ChatHoverEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/ChatHoverEvent.kt
new file mode 100644
index 000000000..38118bac6
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/events/ChatHoverEvent.kt
@@ -0,0 +1,17 @@
+package at.hannibal2.skyhanni.events
+
+import net.minecraft.event.HoverEvent
+import net.minecraft.util.ChatComponentText
+
+/**
+ * This event is mainly used for doing things on chat hover and reading the chat component
+ * of the hovered chat.
+ *
+ * To edit the chat component, add to, or use methods in [GuiChatHook][at.hannibal2.skyhanni.mixins.hooks.GuiChatHook].
+ *
+ * The edited chat component in [GuiChatHook][at.hannibal2.skyhanni.mixins.hooks.GuiChatHook] does not change the actual
+ * chat component, but rather makes a new one just before rendering.
+ */
+class ChatHoverEvent(val component: ChatComponentText) : LorenzEvent() {
+ fun getHoverEvent(): HoverEvent = component.chatStyle.chatHoverEvent
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Cache.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Cache.kt
new file mode 100644
index 000000000..c01c52124
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Cache.kt
@@ -0,0 +1,23 @@
+package at.hannibal2.skyhanni.features.misc.items.enchants
+
+class Cache {
+ var cachedLoreBefore: List<String> = listOf()
+ var cachedLoreAfter: List<String> = listOf()
+
+ // So tooltip gets changed on the same item if the config was changed in the interim
+ var configChanged = false
+
+ fun updateBefore(loreBeforeModification: List<String>) {
+ cachedLoreBefore = loreBeforeModification.toList()
+ }
+
+ fun updateAfter(loreAfterModification: List<String>) {
+ cachedLoreAfter = loreAfterModification.toList()
+ configChanged = false
+ }
+
+ fun isCached(loreBeforeModification: List<String>): Boolean {
+ if (configChanged || loreBeforeModification.size != cachedLoreBefore.size) return false
+ return loreBeforeModification.indices.none { loreBeforeModification[it] != cachedLoreBefore[it] }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Enchant.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Enchant.kt
new file mode 100644
index 000000000..a3069ecdf
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/Enchant.kt
@@ -0,0 +1,77 @@
+package at.hannibal2.skyhanni.features.misc.items.enchants
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import com.google.gson.annotations.Expose
+import java.util.TreeSet
+
+open class Enchant : Comparable<Enchant> {
+ @Expose
+ var nbtName = ""
+
+ @Expose
+ var loreName = ""
+
+ @Expose
+ private var goodLevel = 0
+
+ @Expose
+ private var maxLevel = 0
+
+ private fun isNormal() = this is Normal
+ private fun isUltimate() = this is Ultimate
+ private fun isStacking() = this is Stacking
+
+ open fun getFormattedName(level: Int) = getFormat(level) + loreName
+
+ open fun getFormat(level: Int): String {
+ val config = SkyHanniMod.feature.inventory.enchantParsing
+
+ if (level >= maxLevel) return config.perfectEnchantColor.get().getChatColor()
+ if (level > goodLevel) return config.greatEnchantColor.get().getChatColor()
+ if (level == goodLevel) return config.goodEnchantColor.get().getChatColor()
+ return config.poorEnchantColor.get().getChatColor()
+ }
+
+ override fun toString() = "$nbtName $goodLevel $maxLevel\n"
+
+ override fun compareTo(other: Enchant): Int {
+ if (this.isUltimate() == other.isUltimate()) {
+ if (this.isStacking() == other.isStacking()) {
+ return this.loreName.compareTo(other.loreName)
+ }
+ return if (this.isStacking()) -1 else 1
+ }
+ return if (this.isUltimate()) -1 else 1
+ }
+
+ class Normal : Enchant() {
+ }
+
+ class Ultimate : Enchant() {
+ override fun getFormat(level: Int) = "§d§l"
+ }
+
+ class Stacking : Enchant() {
+ @Expose
+ private var nbtNum: String? = null
+
+ @Expose
+ private var statLabel: String? = null
+
+ @Expose
+ private var stackLevel: TreeSet<Int>? = null
+
+ override fun toString() = "$nbtNum ${stackLevel.toString()} ${super.toString()}"
+ }
+
+ class Dummy(name: String) : Enchant() {
+ init {
+ loreName = name
+ nbtName = name
+ }
+
+ // Ensures enchants not yet in repo stay as vanilla formatting
+ // (instead of that stupid dark red lowercase formatting *cough* sba *cough*)
+ override fun getFormattedName(level: Int) = "§9$loreName"
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantParser.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantParser.kt
new file mode 100644
index 000000000..baf84aff7
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantParser.kt
@@ -0,0 +1,355 @@
+package at.hannibal2.skyhanni.features.misc.items.enchants
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.config.features.inventory.EnchantParsingConfig
+import at.hannibal2.skyhanni.config.features.inventory.EnchantParsingConfig.CommaFormat
+import at.hannibal2.skyhanni.events.ChatHoverEvent
+import at.hannibal2.skyhanni.events.ConfigLoadEvent
+import at.hannibal2.skyhanni.events.LorenzToolTipEvent
+import at.hannibal2.skyhanni.events.RepositoryReloadEvent
+import at.hannibal2.skyhanni.mixins.hooks.GuiChatHook
+import at.hannibal2.skyhanni.utils.ConditionalUtils
+import at.hannibal2.skyhanni.utils.ItemUtils.isEnchanted
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimal
+import at.hannibal2.skyhanni.utils.SkyBlockItemModifierUtils.getEnchantments
+import at.hannibal2.skyhanni.utils.StringUtils.removeColor
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
+import net.minecraft.event.HoverEvent
+import net.minecraft.item.ItemStack
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.IChatComponent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.TreeSet
+
+/**
+ * Modified Enchant Parser from [SkyblockAddons](https://github.com/BiscuitDevelopment/SkyblockAddons/blob/main/src/main/java/codes/biscuit/skyblockaddons/features/enchants/EnchantManager.java)
+ */
+object EnchantParser {
+
+ private val config get() = SkyHanniMod.feature.inventory.enchantParsing
+
+ val patternGroup = RepoPattern.group("misc.items.enchantparsing")
+ val enchantmentPattern by patternGroup.pattern(
+ "enchants", "(?<enchant>[A-Za-z][A-Za-z -]+) (?<levelNumeral>[IVXLCDM]+)(?<stacking>, |\$| \\d{1,3}(,\\d{3})*)"
+ )
+ private val grayEnchantPattern by patternGroup.pattern(
+ "grayenchants", "^(Respiration|Aqua Affinity|Depth Strider|Efficiency).*"
+ )
+
+ private var indexOfLastGrayEnchant = -1
+ private var startEnchant = -1
+ private var endEnchant = -1
+
+ // Stacking enchants with their progress visible should have the
+ // enchants stacked in a single column
+ private var shouldBeSingleColumn = false
+
+ // Used to determine how many enchants are used on each line
+ // for this particular item, since consistency is not Hypixel's strong point
+ private var maxEnchantsPerLine = 0
+ private var loreLines: MutableList<String> = mutableListOf()
+ private var orderedEnchants: TreeSet<FormattedEnchant> = TreeSet()
+
+ private val loreCache: Cache = Cache()
+
+ // Maps for all enchants
+ private var enchants: EnchantsJson = EnchantsJson()
+
+ @SubscribeEvent
+ fun onRepoReload(event: RepositoryReloadEvent) {
+ this.enchants = event.getConstant<EnchantsJson>("Enchants")
+ }
+
+ @SubscribeEvent
+ fun onConfigLoad(event: ConfigLoadEvent) {
+ // Add observers to config options that would need us to mark cache dirty
+ ConditionalUtils.onToggle(
+ config.colorParsing,
+ config.format,
+ config.perfectEnchantColor,
+ config.greatEnchantColor,
+ config.goodEnchantColor,
+ config.poorEnchantColor,
+ config.commaFormat,
+ config.hideVanillaEnchants,
+ config.hideEnchantDescriptions,
+ ) {
+ markCacheDirty()
+ }
+ }
+
+ @SubscribeEvent
+ fun onTooltipEvent(event: LorenzToolTipEvent) {
+ // If enchants doesn't have any enchant data then we have no data to parse enchants correctly
+ if (!isEnabled() || !this.enchants.hasEnchantData()) return
+
+ // The enchants we expect to find in the lore, found from the items NBT data
+ val enchants = event.itemStack.getEnchantments() ?: return
+
+ // Check for any vanilla gray enchants at the top of the tooltip
+ indexOfLastGrayEnchant = accountForAndRemoveGrayEnchants(event.toolTip, event.itemStack)
+
+ parseEnchants(event.toolTip, enchants, null)
+ }
+
+ /**
+ * For tooltips that are shown when hovering over an item from /show
+ */
+ @SubscribeEvent
+ fun onChatHoverEvent(event: ChatHoverEvent) {
+ if (event.getHoverEvent().action != HoverEvent.Action.SHOW_TEXT) return
+ if (!isEnabled() || !this.enchants.hasEnchantData()) return
+
+ val lore = event.getHoverEvent().value.formattedText.split("\n").toMutableList()
+
+ // Since we don't get given an item stack from /show, we pass an empty enchants map and
+ // use all enchants from the Enchants class instead
+ parseEnchants(lore, mapOf(), event.component)
+ }
+
+ private fun parseEnchants(
+ loreList: MutableList<String>,
+ enchants: Map<String, Int>,
+ chatComponent: IChatComponent?,
+ ) {
+ // Check if the lore is already cached so continuous hover isn't 1 fps
+ if (loreCache.isCached(loreList)) {
+ loreList.clear()
+ loreList.addAll(loreCache.cachedLoreAfter)
+ // Need to still set replacement component even if its cached
+ if (chatComponent != null) editChatComponent(chatComponent, loreList)
+ return
+ }
+ loreCache.updateBefore(loreList)
+
+ // Find where the enchants start and end
+ enchantStartAndEnd(loreList, enchants)
+
+ if (endEnchant == -1) {
+ loreCache.updateAfter(loreList)
+ return
+ }
+
+ shouldBeSingleColumn = false
+ loreLines = mutableListOf()
+ orderedEnchants = TreeSet()
+ maxEnchantsPerLine = 0
+
+ // Order all enchants
+ orderEnchants(loreList)
+
+ if (orderedEnchants.isEmpty()) {
+ loreCache.updateAfter(loreList)
+ return
+ }
+
+ // If we have color parsing off and hide enchant descriptions on, remove them and return from method
+ if (!config.colorParsing.get()) {
+ if (config.hideEnchantDescriptions.get()) {
+ loreList.removeAll(loreLines)
+ loreCache.updateAfter(loreList)
+ if (chatComponent != null) editChatComponent(chatComponent, loreList)
+ return
+ }
+ return
+ }
+
+ // Remove enchantment lines so we can insert ours
+ loreList.subList(startEnchant, endEnchant + 1).clear()
+
+ val insertEnchants: MutableList<String> = mutableListOf()
+
+ // Format enchants based on format config option
+ formatEnchants(insertEnchants)
+
+ // Add our parsed enchants back into the lore
+ loreList.addAll(startEnchant, insertEnchants)
+ // Cache parsed lore
+ loreCache.updateAfter(loreList)
+
+ // Alter the chat component value if one was passed
+ if (chatComponent != null) {
+ editChatComponent(chatComponent, loreList)
+ }
+ }
+
+ private fun enchantStartAndEnd(loreList: MutableList<String>, enchants: Map<String, Int>) {
+ var startEnchant = -1
+ var endEnchant = -1
+
+ val startIndex = if (indexOfLastGrayEnchant == -1) 0 else indexOfLastGrayEnchant + 1
+ for (i in startIndex until loreList.size) {
+ val strippedLine = loreList[i].removeColor()
+
+ if (startEnchant == -1) {
+ if (this.enchants.containsEnchantment(enchants, strippedLine)) startEnchant = i
+ } else if (strippedLine.trim().isEmpty() && endEnchant == -1) endEnchant = i - 1
+ }
+
+ this.startEnchant = startEnchant
+ this.endEnchant = endEnchant
+ }
+
+ private fun orderEnchants(loreList: MutableList<String>) {
+ var lastEnchant: FormattedEnchant? = null
+
+ for (i in startEnchant..endEnchant) {
+ val unformattedLine = loreList[i].removeColor()
+ val matcher = enchantmentPattern.matcher(unformattedLine)
+ var containsEnchant = false
+ var enchantsOnThisLine = 0
+
+ while (matcher.find()) {
+ // Pull enchant, enchant level and stacking amount if applicable
+ val enchant = this.enchants.getFromLore(matcher.group("enchant"))
+ val level = matcher.group("levelNumeral").romanToDecimal()
+ val stacking = if (matcher.group("stacking").trimStart().matches("[\\d,]+\$".toRegex())) {
+ shouldBeSingleColumn = true
+ matcher.group("stacking")
+ } else "empty"
+
+ // Last found enchant
+ lastEnchant = FormattedEnchant(enchant, level, stacking)
+
+ if (!orderedEnchants.add(lastEnchant)) {
+ for (e: FormattedEnchant in orderedEnchants) {
+ if (lastEnchant?.let { e.compareTo(it) } == 0) {
+ lastEnchant = e
+ break
+ }
+ }
+ }
+
+ containsEnchant = true
+ enchantsOnThisLine++
+ }
+
+ maxEnchantsPerLine = if (enchantsOnThisLine > maxEnchantsPerLine) enchantsOnThisLine else maxEnchantsPerLine
+
+ if (!containsEnchant && lastEnchant != null) {
+ lastEnchant.addLore(loreList[i])
+ loreLines.add(loreList[i])
+ }
+ }
+ }
+
+ private fun formatEnchants(insertEnchants: MutableList<String>) {
+ // Normal is leaving the formatting as Hypixel provides it
+ if (config.format.get() == EnchantParsingConfig.EnchantFormat.NORMAL) {
+ normalFormatting(insertEnchants)
+ // Compressed is always forcing 3 enchants per line, except when there is stacking enchant progress visible
+ } else if (config.format.get() == EnchantParsingConfig.EnchantFormat.COMPRESSED && !shouldBeSingleColumn) {
+ compressedFormatting(insertEnchants)
+ // Stacked is always forcing 1 enchant per line
+ } else {
+ stackedFormatting(insertEnchants)
+ }
+ }
+
+ private fun normalFormatting(insertEnchants: MutableList<String>) {
+ val commaFormat = config.commaFormat.get()
+ var builder = StringBuilder()
+
+ for ((i, orderedEnchant: FormattedEnchant) in orderedEnchants.withIndex()) {
+ val comma = if (commaFormat == CommaFormat.COPY_ENCHANT) ", " else "§9, "
+
+ builder.append(orderedEnchant.getFormattedString())
+ if (i % maxEnchantsPerLine != maxEnchantsPerLine - 1) {
+ builder.append(comma)
+ } else {
+ insertEnchants.add(builder.toString())
+
+ // This will only add enchant descriptions if there were any to begin with
+ if (!config.hideEnchantDescriptions.get()) insertEnchants.addAll(orderedEnchant.getLore())
+
+ builder = StringBuilder()
+ }
+ }
+
+ finishFormatting(insertEnchants, builder, commaFormat)
+ }
+
+ private fun compressedFormatting(insertEnchants: MutableList<String>) {
+ val commaFormat = config.commaFormat.get()
+ var builder = StringBuilder()
+
+ for ((i, orderedEnchant: FormattedEnchant) in orderedEnchants.withIndex()) {
+ val comma = if (commaFormat == CommaFormat.COPY_ENCHANT) ", " else "§9, "
+
+ builder.append(orderedEnchant.getFormattedString())
+ if (i % 3 != 2) {
+ builder.append(comma)
+ } else {
+ insertEnchants.add(builder.toString())
+ builder = StringBuilder()
+ }
+ }
+
+ finishFormatting(insertEnchants, builder, commaFormat)
+ }
+
+ private fun stackedFormatting(insertEnchants: MutableList<String>) {
+ if (!config.hideEnchantDescriptions.get()) {
+ for (enchant: FormattedEnchant in orderedEnchants) {
+ insertEnchants.add(enchant.getFormattedString())
+ insertEnchants.addAll(enchant.getLore())
+ }
+ } else {
+ for (enchant: FormattedEnchant in orderedEnchants) {
+ insertEnchants.add(enchant.getFormattedString())
+ }
+ }
+ }
+
+ private fun finishFormatting(insertEnchants: MutableList<String>, builder: StringBuilder, commaFormat: CommaFormat) {
+ if (builder.isNotEmpty()) insertEnchants.add(builder.toString())
+
+ // Check if there is a trailing space (therefore also a comma) and remove the last 2 chars
+ if (insertEnchants.last().last() == ' ') {
+ insertEnchants[insertEnchants.lastIndex] =
+ insertEnchants.last().dropLast(if (commaFormat == CommaFormat.COPY_ENCHANT) 2 else 4)
+ }
+ }
+
+ private fun editChatComponent(chatComponent: IChatComponent, loreList: MutableList<String>) {
+ val text = loreList.joinToString("\n").dropLast(2)
+
+ // Just set the component text to the entire lore list instead of reconstructing the entire siblings tree
+ val chatComponentText = ChatComponentText(text)
+ val hoverEvent = HoverEvent(chatComponent.chatStyle.chatHoverEvent.action, chatComponentText)
+
+ GuiChatHook.replaceOnlyHoverEvent(hoverEvent)
+ }
+
+ private fun accountForAndRemoveGrayEnchants(loreList: MutableList<String>, item: ItemStack): Int {
+ // If the item has no enchantmentTagList then there will be no gray enchants
+ if (!item.isEnchanted() || item.enchantmentTagList.tagCount() == 0) return -1
+
+ var lastGrayEnchant = -1
+ val removeGrayEnchants = config.hideVanillaEnchants.get()
+
+ var i = 1
+ for (total in 0 until (1 + item.enchantmentTagList.tagCount())) {
+ val line = loreList[i]
+ if (grayEnchantPattern.matcher(line).matches()) {
+ lastGrayEnchant = i
+
+ if (removeGrayEnchants) loreList.removeAt(i) else i++
+ } else {
+ i++
+ }
+ }
+
+ return if (removeGrayEnchants) -1 else lastGrayEnchant
+ }
+
+ // We don't check if the main toggle here since we still need to go into
+ // the parseEnchants method to deal with hiding vanilla enchants
+ // and enchant descriptions
+ fun isEnabled() = LorenzUtils.inSkyBlock
+
+ private fun markCacheDirty() {
+ loreCache.configChanged = true
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantsJson.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantsJson.kt
new file mode 100644
index 000000000..99fb0b328
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/EnchantsJson.kt
@@ -0,0 +1,41 @@
+package at.hannibal2.skyhanni.features.misc.items.enchants
+
+import at.hannibal2.skyhanni.features.misc.items.enchants.EnchantParser.enchantmentPattern
+import com.google.gson.annotations.Expose
+
+class EnchantsJson {
+ @Expose
+ var NORMAL: HashMap<String, Enchant.Normal> = hashMapOf()
+
+ @Expose
+ var ULTIMATE: HashMap<String, Enchant.Ultimate> = hashMapOf()
+
+ @Expose
+ var STACKING: HashMap<String, Enchant.Stacking> = hashMapOf()
+
+ fun getFromLore(passedLoreName: String): Enchant {
+ val loreName = passedLoreName.lowercase()
+ var enchant: Enchant? = NORMAL[loreName]
+ if (enchant == null) enchant = ULTIMATE[loreName]
+ if (enchant == null) enchant = STACKING[loreName]
+ if (enchant == null) enchant = Enchant.Dummy(passedLoreName)
+ return enchant
+ }
+
+ fun containsEnchantment(enchants: Map<String, Int>, line: String): Boolean {
+ val matcher = enchantmentPattern.matcher(line)
+ while (matcher.find()) {
+ val enchant = this.getFromLore(matcher.group("enchant"))
+ if (enchants.isNotEmpty()) {
+ if (enchants.containsKey(enchant.nbtName)) return true
+ } else {
+ if (NORMAL.containsKey(enchant.loreName.lowercase())) return true
+ if (ULTIMATE.containsKey(enchant.loreName.lowercase())) return true
+ if (STACKING.containsKey(enchant.loreName.lowercase())) return true
+ }
+ }
+ return false
+ }
+
+ fun hasEnchantData() = NORMAL.isNotEmpty() && ULTIMATE.isNotEmpty() && STACKING.isNotEmpty()
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/FormattedEnchant.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/FormattedEnchant.kt
new file mode 100644
index 000000000..504415296
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/items/enchants/FormattedEnchant.kt
@@ -0,0 +1,26 @@
+package at.hannibal2.skyhanni.features.misc.items.enchants
+
+import at.hannibal2.skyhanni.utils.NumberUtil.toRoman
+
+class FormattedEnchant(
+ private val enchant: Enchant,
+ private val level: Int,
+ stacking: String,
+) : Comparable<FormattedEnchant> {
+ private val stacking: String = stacking
+ get() = "§8$field"
+ private val loreDescription: MutableList<String> = mutableListOf()
+
+ fun addLore(lineOfLore: String) = loreDescription.add(lineOfLore)
+
+ fun getLore() = loreDescription
+
+ override fun compareTo(other: FormattedEnchant) = this.enchant.compareTo(other.enchant)
+
+ fun getFormattedString(): String {
+ val builder = StringBuilder()
+ builder.append(enchant.getFormattedName(level)).append(" ").append(level.toRoman())
+
+ return if (!stacking.contains("empty")) builder.append(stacking).toString() else builder.toString()
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiChatHook.kt b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiChatHook.kt
new file mode 100644
index 000000000..354707f85
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiChatHook.kt
@@ -0,0 +1,40 @@
+package at.hannibal2.skyhanni.mixins.hooks
+
+import net.minecraft.event.HoverEvent
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.ChatStyle
+import net.minecraft.util.IChatComponent
+
+object GuiChatHook {
+
+ lateinit var replacement: ChatComponentText
+
+ fun replaceEntireComponent(title: String, chatStyle: ChatStyle) {
+ if (!this::replacement.isInitialized) return
+
+ // Initialise new component
+ val newComponent = ChatComponentText(title)
+ newComponent.setChatStyle(chatStyle)
+
+ replacement = newComponent
+ }
+
+ fun replaceOnlyHoverEvent(hoverEvent: HoverEvent) {
+ if (!this::replacement.isInitialized) return
+
+ // Initialise new component
+ val newComponent = ChatComponentText(replacement.chatComponentText_TextValue)
+ newComponent.setChatStyle(replacement.chatStyle)
+ newComponent.chatStyle.chatHoverEvent = hoverEvent
+
+ replacement = newComponent
+ }
+
+ fun getReplacementAsIChatComponent(): IChatComponent {
+ if (!this::replacement.isInitialized) {
+ // Return an extremely basic chat component as to not error downstream
+ return ChatComponentText("Original component was not set")
+ }
+ return replacement
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinGuiChat.java b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinGuiChat.java
index 5225cf221..97ef80364 100644
--- a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinGuiChat.java
+++ b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinGuiChat.java
@@ -1,16 +1,22 @@
package at.hannibal2.skyhanni.mixins.transformers;
+import at.hannibal2.skyhanni.events.ChatHoverEvent;
import at.hannibal2.skyhanni.features.commands.tabcomplete.TabComplete;
+import at.hannibal2.skyhanni.mixins.hooks.GuiChatHook;
import com.google.common.collect.Lists;
import net.minecraft.client.gui.GuiChat;
import net.minecraft.client.gui.GuiTextField;
+import net.minecraft.util.ChatComponentText;
import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
import org.apache.commons.lang3.StringUtils;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import java.util.List;
@@ -56,4 +62,20 @@ public class MixinGuiChat {
}
}
}
+
+ @Inject(method = "drawScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiChat;handleComponentHover(Lnet/minecraft/util/IChatComponent;II)V"), locals = LocalCapture.CAPTURE_FAILHARD)
+ public void chatHoverEvent(int mouseX, int mouseY, float partialTicks, CallbackInfo ci, IChatComponent component) {
+ // Only ChatComponentText components can make it to this point
+
+ // Always set the replacement, so if someone is no longer editing the replacement
+ // we get the original component back
+ GuiChatHook.INSTANCE.setReplacement((ChatComponentText) component);
+
+ new ChatHoverEvent((ChatComponentText) component).postAndCatch();
+ }
+
+ @ModifyArg(method = "drawScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiChat;handleComponentHover(Lnet/minecraft/util/IChatComponent;II)V"), index = 0)
+ public IChatComponent replaceWithNewComponent(IChatComponent originalComponent) {
+ return GuiChatHook.INSTANCE.getReplacementAsIChatComponent();
+ }
}