aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Cole <40234707+DavidArthurCole@users.noreply.github.com>2024-09-10 15:53:30 -0400
committerGitHub <noreply@github.com>2024-09-10 21:53:30 +0200
commit2f6121bc209fb28e1d9f12fe2d10bfacfb12836d (patch)
tree1dba111a8d401d596e71c2a949e0c0986cbac76b
parente2795490be75dd6e4ddbfa8896d99db956719d3f (diff)
downloadskyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.tar.gz
skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.tar.bz2
skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.zip
Feature: Corpse Tracker (#2306)
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/mining/CorpseTrackerConfig.java24
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/mining/GlaciteMineshaftConfig.java5
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/mining/CorpseLootedEvent.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt (renamed from src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt)2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseTracker.kt142
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt (renamed from src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt)4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt (renamed from src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt)2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/tracker/BucketedItemTrackerData.kt101
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/tracker/ItemTrackerData.kt14
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniBucketedItemTracker.kt214
13 files changed, 513 insertions, 5 deletions
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
@@ -18,6 +18,11 @@ public class GlaciteMineshaftConfig {
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!")
@ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE)
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<MineshaftPityDisplay.PityData> 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<Pair<String, Int>>) : LorenzEvent()
diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt
index e49ca462f..c8ee81288 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseAPI.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseAPI.kt
@@ -1,4 +1,4 @@
-package at.hannibal2.skyhanni.features.mining.mineshaft
+package at.hannibal2.skyhanni.features.mining.glacitemineshaft
import at.hannibal2.skyhanni.data.IslandType
import at.hannibal2.skyhanni.events.LorenzChatEvent
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<CorpseType, BucketData>(
+ "Corpse Tracker",
+ { BucketData() },
+ { it.mining.mineshaft.corpseProfitTracker },
+ { drawDisplay(it) }
+ )
+
+ class BucketData : BucketedItemTrackerData<CorpseType>() {
+ override fun resetItems() {
+ corpsesLooted = EnumMap(CorpseType::class.java)
+ }
+
+ override fun getDescription(timesGained: Long): List<String> {
+ 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) = "<no coins>"
+ override fun getCoinDescription(bucket: CorpseType?, item: TrackedItem): List<String> = listOf("<no coins>")
+
+ @Expose
+ var corpsesLooted: MutableMap<CorpseType, Long> = 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<Searchable> = 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<CorpseType> = bucketData.getSelectedBucket()?.let { listOf(it) }
+ ?: enumValues<CorpseType>().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/mineshaft/CorpseType.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt
index 7ddfca867..42567d5f3 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/CorpseType.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/CorpseType.kt
@@ -1,4 +1,4 @@
-package at.hannibal2.skyhanni.features.mining.mineshaft
+package at.hannibal2.skyhanni.features.mining.glacitemineshaft
import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName
@@ -10,4 +10,6 @@ enum class CorpseType(val displayName: String, private val keyName: String? = nu
;
val key by lazy { keyName?.asInternalName() }
+
+ override fun toString(): String = displayName
}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt
index 102391b87..eed5f2b9d 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/mining/mineshaft/MineshaftCorpseProfitPer.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/mining/glacitemineshaft/MineshaftCorpseProfitPer.kt
@@ -1,4 +1,4 @@
-package at.hannibal2.skyhanni.features.mining.mineshaft
+package at.hannibal2.skyhanni.features.mining.glacitemineshaft
import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.events.mining.CorpseLootedEvent
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<E : Enum<E>> : TrackerData() {
+
+ private val config get() = SkyHanniMod.feature.misc.tracker
+
+ abstract fun resetItems()
+
+ abstract fun getDescription(timesGained: Long): List<String>
+
+ abstract fun getCoinName(bucket: E?, item: TrackedItem): String
+
+ abstract fun getCoinDescription(bucket: E?, item: TrackedItem): List<String>
+
+ 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<E> by lazy {
+ @Suppress("UNCHECKED_CAST")
+ selectedBucket?.javaClass?.enumConstants
+ ?: (this.javaClass.genericSuperclass as? ParameterizedTypeImpl)?.actualTypeArguments?.firstOrNull()?.let { type ->
+ (type as? Class<E>)?.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<E, MutableMap<NEUInternalName, TrackedItem>> = HashMap()
+
+ private fun getBucket(bucket: E): MutableMap<NEUInternalName, TrackedItem> = bucketedItems[bucket]?.toMutableMap() ?: HashMap()
+ private fun getPoppedBuckets(): MutableList<E> = (bucketedItems.toMutableMap().filter { it.value.isNotEmpty() }.keys).toMutableList()
+ fun getItemsProp(): MutableMap<NEUInternalName, TrackedItem> = 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<NEUInternalName, TrackedItem> {
+ val flatMap: MutableMap<NEUInternalName, TrackedItem> = 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<E : Enum<E>, BucketedData : BucketedItemTrackerData<E>>(
+ name: String,
+ createNewSession: () -> BucketedData,
+ getStorage: (ProfileSpecificStorage) -> BucketedData,
+ drawDisplay: (BucketedData) -> List<Searchable>,
+ vararg extraStorage: Pair<DisplayMode, (ProfileSpecificStorage) -> BucketedData>,
+) : SkyHanniTracker<BucketedData>(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<Searchable>) {
+ if (isInventoryOpen()) {
+ lists.addSearchableSelector<ItemPriceSource>(
+ "",
+ 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<Searchable>,
+ 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<Searchable>,
+ ): Double {
+ var profit = 0.0
+ val dataItems = data.getItemsProp()
+ val items = mutableMapOf<NEUInternalName, Long>()
+ 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<String>()
+ 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()
+ }
+}