From 2f6121bc209fb28e1d9f12fe2d10bfacfb12836d Mon Sep 17 00:00:00 2001 From: David Cole <40234707+DavidArthurCole@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:53:30 -0400 Subject: Feature: Corpse Tracker (#2306) --- .../hannibal2/skyhanni/config/commands/Commands.kt | 2 + .../features/mining/CorpseTrackerConfig.java | 24 +++ .../features/mining/GlaciteMineshaftConfig.java | 5 + .../config/storage/ProfileSpecificStorage.java | 4 + .../skyhanni/events/mining/CorpseLootedEvent.kt | 2 +- .../features/mining/glacitemineshaft/CorpseAPI.kt | 87 +++++++++ .../mining/glacitemineshaft/CorpseTracker.kt | 142 ++++++++++++++ .../features/mining/glacitemineshaft/CorpseType.kt | 15 ++ .../glacitemineshaft/MineshaftCorpseProfitPer.kt | 56 ++++++ .../features/mining/mineshaft/CorpseAPI.kt | 87 --------- .../features/mining/mineshaft/CorpseType.kt | 13 -- .../mining/mineshaft/MineshaftCorpseProfitPer.kt | 56 ------ .../at/hannibal2/skyhanni/utils/CollectionUtils.kt | 2 +- .../utils/tracker/BucketedItemTrackerData.kt | 101 ++++++++++ .../skyhanni/utils/tracker/ItemTrackerData.kt | 14 ++ .../utils/tracker/SkyHanniBucketedItemTracker.kt | 214 +++++++++++++++++++++ 16 files changed, 666 insertions(+), 158 deletions(-) create mode 100644 src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java create mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt delete mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt delete mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt delete mode 100644 src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt (limited to 'src/main/java') diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt index edfdb2d7a..7a17c1462 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -58,6 +58,7 @@ import at.hannibal2.skyhanni.features.garden.visitor.GardenVisitorDropStatistics import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker import at.hannibal2.skyhanni.features.mining.KingTalismanHelper import at.hannibal2.skyhanni.features.mining.MineshaftPityDisplay +import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseTracker import at.hannibal2.skyhanni.features.mining.fossilexcavator.ExcavatorProfitTracker import at.hannibal2.skyhanni.features.mining.powdertracker.PowderTracker import at.hannibal2.skyhanni.features.minion.MinionFeatures @@ -219,6 +220,7 @@ object Commands { registerCommand("shresetghostcounter", "Resets the ghost counter") { GhostUtil.reset() } registerCommand("shresetpowdertracker", "Resets the Powder Tracker") { PowderTracker.resetCommand() } registerCommand("shresetdicertracker", "Resets the Dicer Drop Tracker") { DicerRngDropTracker.resetCommand() } + registerCommand("shresetcorpsetracker", "Resets the Glacite Mineshaft Corpse Tracker") { CorpseTracker.resetCommand() } registerCommand( "shresetendernodetracker", "Resets the Ender Node Tracker", diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java new file mode 100644 index 000000000..61d3b1959 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java @@ -0,0 +1,24 @@ +package at.hannibal2.skyhanni.config.features.mining; + +import at.hannibal2.skyhanni.config.core.config.Position; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigLink; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; + +public class CorpseTrackerConfig { + + @Expose + @ConfigOption(name = "Enabled", desc = "Enable the Corpse Tracker overlay for Glacite Mineshafts.") + @ConfigEditorBoolean + public boolean enabled = false; + + @Expose + @ConfigOption(name = "Only when in Mineshaft", desc = "Only show the overlay while in a Glacite Mineshaft.") + @ConfigEditorBoolean + public boolean onlyInMineshaft = false; + + @Expose + @ConfigLink(owner = CorpseTrackerConfig.class, field = "enabled") + public Position position = new Position(-274, 0, false, true); +} diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java index b89a7d3d0..723cf9b90 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java @@ -17,6 +17,11 @@ public class GlaciteMineshaftConfig { @Accordion public CorpseLocatorConfig corpseLocator = new CorpseLocatorConfig(); + @Expose + @ConfigOption(name = "Corpse Tracker", desc = "") + @Accordion + public CorpseTrackerConfig corpseTracker = new CorpseTrackerConfig(); + @Expose @ConfigOption(name = "Share Waypoint Location", desc = "Share the location of the nearest waypoint upon key press.\n" + "§eYou can share the location even if it has already been shared!") diff --git a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java index d622d317a..5938eebdb 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -33,6 +33,7 @@ import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactor import at.hannibal2.skyhanni.features.inventory.wardrobe.WardrobeAPI; import at.hannibal2.skyhanni.features.mining.MineshaftPityDisplay; import at.hannibal2.skyhanni.features.mining.fossilexcavator.ExcavatorProfitTracker; +import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseTracker; import at.hannibal2.skyhanni.features.mining.powdertracker.PowderTracker; import at.hannibal2.skyhanni.features.misc.trevor.TrevorTracker; import at.hannibal2.skyhanni.features.rift.area.westvillage.VerminTracker; @@ -564,6 +565,9 @@ public class ProfileSpecificStorage { @Expose public List blocksBroken = new ArrayList<>(); + + @Expose + public CorpseTracker.BucketData corpseProfitTracker = new CorpseTracker.BucketData(); } } diff --git a/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt index ea1e813d0..9e38d8e95 100644 --- a/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt +++ b/src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt @@ -1,6 +1,6 @@ package at.hannibal2.skyhanni.events.mining import at.hannibal2.skyhanni.events.LorenzEvent -import at.hannibal2.skyhanni.features.mining.mineshaft.CorpseType +import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseType class CorpseLootedEvent(val corpseType: CorpseType, val loot: List>) : LorenzEvent() diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt new file mode 100644 index 000000000..c8ee81288 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt @@ -0,0 +1,87 @@ +package at.hannibal2.skyhanni.features.mining.glacitemineshaft + +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.events.LorenzChatEvent +import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.ItemUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland +import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matches +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +@SkyHanniModule +object CorpseAPI { + + private val patternGroup = RepoPattern.group("mining.mineshaft") + private val chatPatternGroup = patternGroup.group("chat") + + /** + * REGEX-TEST: §r§b§l§r§9§lLAPIS §r§b§lCORPSE LOOT! + * REGEX-TEST: §r§b§l§r§7§lTUNGSTEN §r§b§lCORPSE LOOT! + * REGEX-TEST: §r§b§l§r§6§lUMBER §r§b§lCORPSE LOOT! + * REGEX-TEST: §r§b§l§r§f§lVANGUARD §r§b§lCORPSE LOOT! + */ + private val startPattern by chatPatternGroup.pattern( + "start", + " {2}§r§b§l§r§(?.)§l(?.*) §r§b§lCORPSE LOOT! ?" + ) + + /** + * REGEX-TEST: §a§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ + */ + private val endPattern by chatPatternGroup.pattern("end", "§a§l▬{64}") + + /** + * REGEX-TEST: §r§9☠ Fine Onyx Gemstone §r§8x2 + */ + private val itemPattern by chatPatternGroup.pattern("item", " {4}§r(?.+)") + + private var inLoot = false + private val loot = mutableListOf>() + + private var corpseType: CorpseType? = null + + @SubscribeEvent + fun onChat(event: LorenzChatEvent) { + if (!IslandType.MINESHAFT.isInIsland()) return + + val message = event.message + + startPattern.matchMatcher(message) { + inLoot = true + val name = group("name") + corpseType = CorpseType.valueOf(name) + return + } + + if (!inLoot) return + + if (endPattern.matches(message)) { + corpseType?.let { + CorpseLootedEvent(it, loot.toList()).postAndCatch() + } + corpseType = null + loot.clear() + inLoot = false + return + } + var pair = itemPattern.matchMatcher(message) { + /** + * TODO fix the bug that readItemAmount produces two different outputs: + * §r§fEnchanted Book -> §fEnchanted + * §fEnchanted Book §r§8x -> §fEnchanted Book + * + * also maybe this is no bug, as enchanted book is no real item? + */ + ItemUtils.readItemAmount(group("item")) + } ?: return + // Workaround: If it is an enchanted book, we assume it is a paleontologist I book + if (pair.first.let { it == "§fEnchanted" || it == "§fEnchanted Book" }) { +// pair = "Paleontologist I" to pair.second + pair = "§9Ice Cold I" to pair.second + } + loot.add(pair) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt new file mode 100644 index 000000000..77aabc1bd --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt @@ -0,0 +1,142 @@ +package at.hannibal2.skyhanni.features.mining.glacitemineshaft + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.IslandChangeEvent +import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.CollectionUtils.addOrPut +import at.hannibal2.skyhanni.utils.CollectionUtils.addSearchString +import at.hannibal2.skyhanni.utils.CollectionUtils.sumAllValues +import at.hannibal2.skyhanni.utils.ItemPriceUtils.getPrice +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.renderables.Searchable +import at.hannibal2.skyhanni.utils.renderables.toSearchable +import at.hannibal2.skyhanni.utils.tracker.BucketedItemTrackerData +import at.hannibal2.skyhanni.utils.tracker.ItemTrackerData.TrackedItem +import at.hannibal2.skyhanni.utils.tracker.SkyHanniBucketedItemTracker +import com.google.gson.annotations.Expose +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.EnumMap + +@SkyHanniModule +object CorpseTracker { + private val config get() = SkyHanniMod.feature.mining.glaciteMineshaft.corpseTracker + + private val tracker = SkyHanniBucketedItemTracker( + "Corpse Tracker", + { BucketData() }, + { it.mining.mineshaft.corpseProfitTracker }, + { drawDisplay(it) } + ) + + class BucketData : BucketedItemTrackerData() { + override fun resetItems() { + corpsesLooted = EnumMap(CorpseType::class.java) + } + + override fun getDescription(timesGained: Long): List { + val divisor = 1.coerceAtLeast(getSelectedBucket()?.let { corpsesLooted[it]?.toInt() } ?: corpsesLooted.sumAllValues().toInt()) + val percentage = timesGained.toDouble() / divisor + val dropRate = LorenzUtils.formatPercentage(percentage.coerceAtMost(1.0)) + return listOf( + "§7Dropped §e${timesGained.addSeparators()} §7times.", + "§7Your drop rate: §c$dropRate.", + ) + } + + override fun getCoinName(bucket: CorpseType?, item: TrackedItem) = "" + override fun getCoinDescription(bucket: CorpseType?, item: TrackedItem): List = listOf("") + + @Expose + var corpsesLooted: MutableMap = EnumMap(CorpseType::class.java) + + fun getCorpseCount(): Long = getSelectedBucket()?.let { corpsesLooted[it] } ?: corpsesLooted.values.sum() + } + + private fun addLootedCorpse(type: CorpseType) = tracker.modify { it.corpsesLooted.addOrPut(type, 1) } + + @SubscribeEvent + fun onCorpseLoot(event: CorpseLootedEvent) { + addLootedCorpse(event.corpseType) + for ((itemName, amount) in event.loot) { + NEUInternalName.fromItemNameOrNull(itemName)?.let { item -> + tracker.modify { + it.addItem(event.corpseType, item, amount) + } + } + } + } + + private fun drawDisplay(bucketData: BucketData): List = buildList { + addSearchString("§b§lMineshaft Corpse Profit Tracker") + tracker.addBucketSelector(this, bucketData, "Corpse Type") + + if (bucketData.getCorpseCount() == 0L) return@buildList + + var profit = tracker.drawItems(bucketData, { true }, this) + val applicableKeys: List = bucketData.getSelectedBucket()?.let { listOf(it) } + ?: enumValues().toList().filter { bucketData.corpsesLooted[it] != null } + var totalKeyCost = 0.0 + var totalKeyCount = 0 + val keyCostStrings = buildList { + applicableKeys.forEach { keyData -> + keyData.key?.let { key -> + val keyName = key.itemName + val price = key.getPrice() + val count = bucketData.corpsesLooted[keyData] ?: 0 + val totalPrice = price * count + if (totalPrice > 0) { + profit -= totalPrice + totalKeyCost += totalPrice + totalKeyCount += count.toInt() + add("§7${count}x $keyName§7: §c-${totalPrice.shortFormat()}") + } + } + } + } + + if (totalKeyCount > 0) { + val specificKeyFormat = if (applicableKeys.count() == 1) applicableKeys.first().key!!.itemName else "§eCorpse Keys" + val keyFormat = "§7${totalKeyCount}x $specificKeyFormat§7: §c-${totalKeyCost.shortFormat()}" + add( + if (applicableKeys.count() == 1) Renderable.string(keyFormat).toSearchable() + else Renderable.hoverTips( + keyFormat, + keyCostStrings, + ).toSearchable(), + ) + } + + add(tracker.addTotalProfit(profit, bucketData.getCorpseCount(), "loot")) + + tracker.addPriceFromButton(this) + } + + @SubscribeEvent + fun onRenderOverlay(event: GuiRenderEvent) { + if (!isEnabled()) return + tracker.renderDisplay(config.position) + } + + @SubscribeEvent + fun onIslandChange(event: IslandChangeEvent) { + if (event.newIsland == IslandType.MINESHAFT || event.newIsland == IslandType.DWARVEN_MINES) { + tracker.firstUpdate() + } + } + + fun resetCommand() { + tracker.resetCommand() + } + + fun isEnabled() = + config.enabled && IslandType.DWARVEN_MINES.isInIsland() && (!config.onlyInMineshaft || IslandType.MINESHAFT.isInIsland()) +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt new file mode 100644 index 000000000..42567d5f3 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt @@ -0,0 +1,15 @@ +package at.hannibal2.skyhanni.features.mining.glacitemineshaft + +import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName + +enum class CorpseType(val displayName: String, private val keyName: String? = null) { + LAPIS("§9Lapis"), + TUNGSTEN("§7Tungsten", "TUNGSTEN_KEY"), + UMBER("§6Umber", "UMBER_KEY"), + VANGUARD("§fVanguard", "SKELETON_KEY"), + ; + + val key by lazy { keyName?.asInternalName() } + + override fun toString(): String = displayName +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt new file mode 100644 index 000000000..eed5f2b9d --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt @@ -0,0 +1,56 @@ +package at.hannibal2.skyhanni.features.mining.glacitemineshaft + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.CollectionUtils.sortedDesc +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.NEUItems.getPrice +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +@SkyHanniModule +object MineshaftCorpseProfitPer { + private val config get() = SkyHanniMod.feature.mining.mineshaft + + @SubscribeEvent + fun onFossilExcavation(event: CorpseLootedEvent) { + if (!config.profitPerCorpseLoot) return + val loot = event.loot + + var totalProfit = 0.0 + val map = mutableMapOf() + for ((name, amount) in loot) { + if (name == "§bGlacite Powder") continue + NEUInternalName.fromItemNameOrNull(name)?.let { + val pricePer = it.getPrice() + if (pricePer == -1.0) continue + val profit = amount * pricePer + val text = "§eFound $name §8${amount.addSeparators()}x §7(§6${profit.shortFormat()}§7)" + map[text] = profit + totalProfit += profit + } + } + + val corpseType = event.corpseType + val name = corpseType.displayName + + corpseType.key?.let { + val keyName = it.itemName + val price = it.getPrice() + + map["$keyName: §c-${price.shortFormat()}"] = -price + totalProfit -= price + } + + val hover = map.sortedDesc().keys.toMutableList() + val profitPrefix = if (totalProfit < 0) "§c" else "§6" + val totalMessage = "Profit for $name Corpse§e: $profitPrefix${totalProfit.shortFormat()}" + hover.add("") + hover.add("§e$totalMessage") + ChatUtils.hoverableChat(totalMessage, hover) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt deleted file mode 100644 index e49ca462f..000000000 --- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt +++ /dev/null @@ -1,87 +0,0 @@ -package at.hannibal2.skyhanni.features.mining.mineshaft - -import at.hannibal2.skyhanni.data.IslandType -import at.hannibal2.skyhanni.events.LorenzChatEvent -import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent -import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.utils.ItemUtils -import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland -import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher -import at.hannibal2.skyhanni.utils.RegexUtils.matches -import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent - -@SkyHanniModule -object CorpseAPI { - - private val patternGroup = RepoPattern.group("mining.mineshaft") - private val chatPatternGroup = patternGroup.group("chat") - - /** - * REGEX-TEST: §r§b§l§r§9§lLAPIS §r§b§lCORPSE LOOT! - * REGEX-TEST: §r§b§l§r§7§lTUNGSTEN §r§b§lCORPSE LOOT! - * REGEX-TEST: §r§b§l§r§6§lUMBER §r§b§lCORPSE LOOT! - * REGEX-TEST: §r§b§l§r§f§lVANGUARD §r§b§lCORPSE LOOT! - */ - private val startPattern by chatPatternGroup.pattern( - "start", - " {2}§r§b§l§r§(?.)§l(?.*) §r§b§lCORPSE LOOT! ?" - ) - - /** - * REGEX-TEST: §a§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ - */ - private val endPattern by chatPatternGroup.pattern("end", "§a§l▬{64}") - - /** - * REGEX-TEST: §r§9☠ Fine Onyx Gemstone §r§8x2 - */ - private val itemPattern by chatPatternGroup.pattern("item", " {4}§r(?.+)") - - private var inLoot = false - private val loot = mutableListOf>() - - private var corpseType: CorpseType? = null - - @SubscribeEvent - fun onChat(event: LorenzChatEvent) { - if (!IslandType.MINESHAFT.isInIsland()) return - - val message = event.message - - startPattern.matchMatcher(message) { - inLoot = true - val name = group("name") - corpseType = CorpseType.valueOf(name) - return - } - - if (!inLoot) return - - if (endPattern.matches(message)) { - corpseType?.let { - CorpseLootedEvent(it, loot.toList()).postAndCatch() - } - corpseType = null - loot.clear() - inLoot = false - return - } - var pair = itemPattern.matchMatcher(message) { - /** - * TODO fix the bug that readItemAmount produces two different outputs: - * §r§fEnchanted Book -> §fEnchanted - * §fEnchanted Book §r§8x -> §fEnchanted Book - * - * also maybe this is no bug, as enchanted book is no real item? - */ - ItemUtils.readItemAmount(group("item")) - } ?: return - // Workaround: If it is an enchanted book, we assume it is a paleontologist I book - if (pair.first.let { it == "§fEnchanted" || it == "§fEnchanted Book" }) { -// pair = "Paleontologist I" to pair.second - pair = "§9Ice Cold I" to pair.second - } - loot.add(pair) - } -} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt deleted file mode 100644 index 7ddfca867..000000000 --- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt +++ /dev/null @@ -1,13 +0,0 @@ -package at.hannibal2.skyhanni.features.mining.mineshaft - -import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName - -enum class CorpseType(val displayName: String, private val keyName: String? = null) { - LAPIS("§9Lapis"), - TUNGSTEN("§7Tungsten", "TUNGSTEN_KEY"), - UMBER("§6Umber", "UMBER_KEY"), - VANGUARD("§fVanguard", "SKELETON_KEY"), - ; - - val key by lazy { keyName?.asInternalName() } -} diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt deleted file mode 100644 index 102391b87..000000000 --- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt +++ /dev/null @@ -1,56 +0,0 @@ -package at.hannibal2.skyhanni.features.mining.mineshaft - -import at.hannibal2.skyhanni.SkyHanniMod -import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent -import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.utils.ChatUtils -import at.hannibal2.skyhanni.utils.CollectionUtils.sortedDesc -import at.hannibal2.skyhanni.utils.ItemUtils.itemName -import at.hannibal2.skyhanni.utils.NEUInternalName -import at.hannibal2.skyhanni.utils.NEUItems.getPrice -import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators -import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent - -@SkyHanniModule -object MineshaftCorpseProfitPer { - private val config get() = SkyHanniMod.feature.mining.mineshaft - - @SubscribeEvent - fun onFossilExcavation(event: CorpseLootedEvent) { - if (!config.profitPerCorpseLoot) return - val loot = event.loot - - var totalProfit = 0.0 - val map = mutableMapOf() - for ((name, amount) in loot) { - if (name == "§bGlacite Powder") continue - NEUInternalName.fromItemNameOrNull(name)?.let { - val pricePer = it.getPrice() - if (pricePer == -1.0) continue - val profit = amount * pricePer - val text = "§eFound $name §8${amount.addSeparators()}x §7(§6${profit.shortFormat()}§7)" - map[text] = profit - totalProfit += profit - } - } - - val corpseType = event.corpseType - val name = corpseType.displayName - - corpseType.key?.let { - val keyName = it.itemName - val price = it.getPrice() - - map["$keyName: §c-${price.shortFormat()}"] = -price - totalProfit -= price - } - - val hover = map.sortedDesc().keys.toMutableList() - val profitPrefix = if (totalProfit < 0) "§c" else "§6" - val totalMessage = "Profit for $name Corpse§e: $profitPrefix${totalProfit.shortFormat()}" - hover.add("") - hover.add("§e$totalMessage") - ChatUtils.hoverableChat(totalMessage, hover) - } -} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt index 991b1a53e..898466ce4 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt @@ -336,7 +336,7 @@ object CollectionUtils { for (entry in universe) { val display = getName(entry) if (isCurrent(entry)) { - addString("§a[$display]") + addString("§a[$display§a]") } else { addString("§e[") add( diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt new file mode 100644 index 000000000..3bedaf8a4 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt @@ -0,0 +1,101 @@ +package at.hannibal2.skyhanni.utils.tracker + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.tracker.ItemTrackerData.TrackedItem +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + +abstract class BucketedItemTrackerData> : TrackerData() { + + private val config get() = SkyHanniMod.feature.misc.tracker + + abstract fun resetItems() + + abstract fun getDescription(timesGained: Long): List + + abstract fun getCoinName(bucket: E?, item: TrackedItem): String + + abstract fun getCoinDescription(bucket: E?, item: TrackedItem): List + + open fun getCustomPricePer(internalName: NEUInternalName) = SkyHanniTracker.getPricePer(internalName) + + override fun reset() { + bucketedItems.clear() + selectedBucket = null + resetItems() + } + + fun addItem(bucket: E, internalName: NEUInternalName, stackSize: Int) { + val bucketMap = bucketedItems.getOrPut(bucket) { HashMap() } + val item = bucketMap.getOrPut(internalName) { TrackedItem() } + + item.timesGained++ + item.totalAmount += stackSize + item.lastTimeUpdated = SimpleTimeMark.now() + } + + fun removeItem(bucket: E?, internalName: NEUInternalName) { + bucket?.let { + bucketedItems[bucket]?.remove(internalName) + } ?: bucketedItems.forEach { + it.value.remove(internalName) + } + } + + fun toggleItemHide(bucket: E?, internalName: NEUInternalName) { + bucket?.let { + bucketedItems[bucket]?.get(internalName)?.let { it.hidden = !it.hidden } + } ?: bucketedItems.forEach { + it.value[internalName]?.hidden = !it.value[internalName]?.hidden!! + } + } + + private val buckets: Array by lazy { + @Suppress("UNCHECKED_CAST") + selectedBucket?.javaClass?.enumConstants + ?: (this.javaClass.genericSuperclass as? ParameterizedTypeImpl)?.actualTypeArguments?.firstOrNull()?.let { type -> + (type as? Class)?.enumConstants + } ?: ErrorManager.skyHanniError( + "Unable to retrieve enum constants for E in BucketedItemTrackerData", + "selectedBucket" to selectedBucket, + "dataClass" to this.javaClass.superclass.name, + ) + } + + private var selectedBucket: E? = null + private var bucketedItems: MutableMap> = HashMap() + + private fun getBucket(bucket: E): MutableMap = bucketedItems[bucket]?.toMutableMap() ?: HashMap() + private fun getPoppedBuckets(): MutableList = (bucketedItems.toMutableMap().filter { it.value.isNotEmpty() }.keys).toMutableList() + fun getItemsProp(): MutableMap = getSelectedBucket()?.let { getBucket(it) } ?: flattenBuckets() + fun getSelectedBucket() = selectedBucket + fun selectNextSequentialBucket() { + // Move to the next ordinal, or wrap to null if at the last value + val nextOrdinal = selectedBucket?.let { it.ordinal + 1 } // Only calculate if selectedBucket is non-null + selectedBucket = when { + selectedBucket == null -> buckets.first() // If selectedBucket is null, start with the first enum + nextOrdinal != null && nextOrdinal >= buckets.size -> null // Wrap to null if we've reached the end + nextOrdinal != null -> buckets[nextOrdinal] // Move to the next enum value + else -> selectedBucket // Fallback, shouldn't happen + } + } + + private fun flattenBuckets(): MutableMap { + val flatMap: MutableMap = HashMap() + getPoppedBuckets().distinct().forEach { bucket -> + getBucket(bucket).filter { !it.value.hidden }.entries.distinctBy { it.key }.forEach { (key, value) -> + flatMap.merge(key, value) { existing, new -> + existing.copy( + hidden = false, + totalAmount = existing.totalAmount + new.totalAmount, + timesGained = existing.timesGained + new.timesGained, + lastTimeUpdated = maxOf(existing.lastTimeUpdated, new.lastTimeUpdated), + ) + } + } + } + return flatMap.toMutableMap() + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt index bce63e2d1..a3976e44f 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt @@ -52,5 +52,19 @@ abstract class ItemTrackerData : TrackerData() { var hidden = false var lastTimeUpdated = SimpleTimeMark.farPast() + + fun copy( + timesGained: Long = this.timesGained, + totalAmount: Long = this.totalAmount, + hidden: Boolean = this.hidden, + lastTimeUpdated: SimpleTimeMark = this.lastTimeUpdated, + ): TrackedItem { + val copy = TrackedItem() + copy.timesGained = timesGained + copy.totalAmount = totalAmount + copy.hidden = hidden + copy.lastTimeUpdated = lastTimeUpdated + return copy + } } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt new file mode 100644 index 000000000..dcbab2b1b --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt @@ -0,0 +1,214 @@ +package at.hannibal2.skyhanni.utils.tracker + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage +import at.hannibal2.skyhanni.data.SlayerAPI +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.CollectionUtils.addSearchableSelector +import at.hannibal2.skyhanni.utils.CollectionUtils.sortedDesc +import at.hannibal2.skyhanni.utils.ItemPriceSource +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.KeyboardManager +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NEUInternalName +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.renderables.RenderableUtils.addButton +import at.hannibal2.skyhanni.utils.renderables.Searchable +import at.hannibal2.skyhanni.utils.renderables.toSearchable +import kotlin.time.Duration.Companion.seconds + +class SkyHanniBucketedItemTracker, BucketedData : BucketedItemTrackerData>( + name: String, + createNewSession: () -> BucketedData, + getStorage: (ProfileSpecificStorage) -> BucketedData, + drawDisplay: (BucketedData) -> List, + vararg extraStorage: Pair BucketedData>, +) : SkyHanniTracker(name, createNewSession, getStorage, *extraStorage, drawDisplay = drawDisplay) { + + companion object { + val SKYBLOCK_COIN = NEUInternalName.SKYBLOCK_COIN + } + + fun addCoins(bucket: E, coins: Int) { + addItem(bucket, SKYBLOCK_COIN, coins) + } + + fun addItem(bucket: E, internalName: NEUInternalName, amount: Int) { + modify { + it.addItem(bucket, internalName, amount) + } + getSharedTracker()?.let { + val hidden = it.get(DisplayMode.TOTAL).getItemsProp()[internalName]!!.hidden + it.get(DisplayMode.SESSION).getItemsProp()[internalName]!!.hidden = hidden + } + + val (itemName, price) = SlayerAPI.getItemNameAndPrice(internalName, amount) + if (config.warnings.chat && price >= config.warnings.minimumChat) { + ChatUtils.chat("§a+Tracker Drop§7: §r$itemName") + } + if (config.warnings.title && price >= config.warnings.minimumTitle) { + LorenzUtils.sendTitle("§a+ $itemName", 5.seconds) + } + } + + fun addPriceFromButton(lists: MutableList) { + if (isInventoryOpen()) { + lists.addSearchableSelector( + "", + getName = { type -> type.sellName }, + isCurrent = { it?.ordinal == config.priceSource.ordinal }, // todo avoid ordinal + onChange = { + config.priceSource = it?.let { ItemPriceSource.entries[it.ordinal] } // todo avoid ordinal + update() + }, + ) + } + } + + fun addBucketSelector( + lists: MutableList, + data: BucketedData, + sourceStringPrefix: String, + nullBucketLabel: String = "All", + ) { + if (isInventoryOpen()) { + lists.addButton( + prefix = "§7$sourceStringPrefix: ", + getName = data.getSelectedBucket()?.toString() ?: nullBucketLabel, + onChange = { + data.selectNextSequentialBucket() + update() + }, + ) + } + } + + fun drawItems( + data: BucketedData, + filter: (NEUInternalName) -> Boolean, + lists: MutableList, + ): Double { + var profit = 0.0 + val dataItems = data.getItemsProp() + val items = mutableMapOf() + for ((internalName, itemProfit) in dataItems) { + if (!filter(internalName)) continue + + val amount = itemProfit.totalAmount + val pricePer = + if (internalName == SKYBLOCK_COIN) 1.0 else data.getCustomPricePer(internalName) + val price = (pricePer * amount).toLong() + val hidden = itemProfit.hidden + + if (isInventoryOpen() || !hidden) { + items[internalName] = price + } + if (!hidden || !config.excludeHiddenItemsInPrice) { + profit += price + } + } + + val limitList = config.hideCheapItems + var pos = 0 + val hiddenItemTexts = mutableListOf() + for ((internalName, price) in items.sortedDesc()) { + val itemProfit = data.getItemsProp()[internalName] ?: error("Item not found for $internalName") + + val amount = itemProfit.totalAmount + val displayAmount = if (internalName == SKYBLOCK_COIN) itemProfit.timesGained else amount + + val cleanName = if (internalName == SKYBLOCK_COIN) { + data.getCoinName(data.getSelectedBucket(), itemProfit) + } else { + internalName.itemName + } + + val priceFormat = price.shortFormat() + val hidden = itemProfit.hidden + val newDrop = itemProfit.lastTimeUpdated.passedSince() < 10.seconds && config.showRecentDrops + val numberColor = if (newDrop) "§a§l" else "§7" + + var displayName = if (hidden) { + "§8§m" + cleanName.removeColor(keepFormatting = true).replace("§r", "") + } else cleanName + displayName = " $numberColor${displayAmount.addSeparators()}x $displayName§7: §6$priceFormat" + + pos++ + if (limitList.enabled.get()) { + if (pos > limitList.alwaysShowBest.get()) { + if (price < limitList.minPrice.get() * 1000) { + hiddenItemTexts += displayName + continue + } + } + } + + val lore = buildLore(data, itemProfit, hidden, newDrop, internalName) + val renderable = if (isInventoryOpen()) Renderable.clickAndHover( + displayName, lore, + onClick = { + if (KeyboardManager.isModifierKeyDown()) { + data.removeItem(data.getSelectedBucket(), internalName) + ChatUtils.chat("Removed $cleanName §efrom $name${if (data.getSelectedBucket() != null) " (${data.getSelectedBucket()})" else ""}") + } else { + modify { + it.toggleItemHide(data.getSelectedBucket(), internalName) + } + } + update() + + }, + ) else Renderable.string(displayName) + + lists.add(renderable.toSearchable(name)) + } + if (hiddenItemTexts.size > 0) { + val text = Renderable.hoverTips(" §7${hiddenItemTexts.size} cheap items are hidden.", hiddenItemTexts).toSearchable() + lists.add(text) + } + + return profit + } + + private fun buildLore( + data: BucketedData, + item: ItemTrackerData.TrackedItem, + hidden: Boolean, + newDrop: Boolean, + internalName: NEUInternalName, + ) = buildList { + if (internalName == SKYBLOCK_COIN) { + addAll(data.getCoinDescription(data.getSelectedBucket(), item)) + } else { + addAll(data.getDescription(item.timesGained)) + } + add("") + if (newDrop) { + add("§aYou obtained this item recently.") + add("") + } + add("§eClick to " + (if (hidden) "show" else "hide") + "!") + add("§eControl + Click to remove this item!") + if (SkyHanniMod.feature.dev.debug.enabled) { + add("") + add("§7${internalName}") + } + } + + fun addTotalProfit(profit: Double, totalAmount: Long, action: String): Searchable { + val profitFormat = profit.toLong().addSeparators() + val profitPrefix = if (profit < 0) "§c" else "§6" + + val tips = if (totalAmount > 0) { + val profitPerCatch = profit / totalAmount + val profitPerCatchFormat = profitPerCatch.shortFormat() + listOf("§7Profit per $action: $profitPrefix$profitPerCatchFormat") + } else emptyList() + + val text = "§eTotal Profit: $profitPrefix$profitFormat coins" + return Renderable.hoverTips(text, tips).toSearchable() + } +} -- cgit