aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/utils
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 /src/main/java/at/hannibal2/skyhanni/utils
parente2795490be75dd6e4ddbfa8896d99db956719d3f (diff)
downloadskyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.tar.gz
skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.tar.bz2
skyhanni-2f6121bc209fb28e1d9f12fe2d10bfacfb12836d.zip
Feature: Corpse Tracker (#2306)
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/utils')
-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
4 files changed, 330 insertions, 1 deletions
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()
+ }
+}