aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/features
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 /src/main/java/at/hannibal2/skyhanni/features
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>
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/features')
-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
5 files changed, 522 insertions, 0 deletions
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()
+ }
+}