diff options
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(); + } } |