aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorhannibal2 <24389977+hannibal002@users.noreply.github.com>2023-11-24 03:01:01 +0100
committerGitHub <noreply@github.com>2023-11-24 03:01:01 +0100
commit43f0e576509d54bb7b12ab59be292c261a0b050d (patch)
treea61aa63d2393ac88e1ddbf577a67065a2753efaa /src/main/java
parentdd2a77f37504f81e09806e1c277451957ea455ac (diff)
downloadskyhanni-43f0e576509d54bb7b12ab59be292c261a0b050d.tar.gz
skyhanni-43f0e576509d54bb7b12ab59be292c261a0b050d.tar.bz2
skyhanni-43f0e576509d54bb7b12ab59be292c261a0b050d.zip
fishing v2 (#729)
Prevent wrong fishing item pickup #729
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/api/CollectionAPI.kt5
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/MinecraftData.kt7
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/OwnInventoryData.kt68
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/SkillExperience.kt10
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/ItemInHandChangeEvent.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/SkillExpGainEvent.kt4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/fishing/FishingAPI.kt4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/fishing/FishingProfitTracker.kt117
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/DelayedRun.kt21
9 files changed, 189 insertions, 50 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/api/CollectionAPI.kt b/src/main/java/at/hannibal2/skyhanni/api/CollectionAPI.kt
index cd7fa0c59..8698d9f28 100644
--- a/src/main/java/at/hannibal2/skyhanni/api/CollectionAPI.kt
+++ b/src/main/java/at/hannibal2/skyhanni/api/CollectionAPI.kt
@@ -70,8 +70,11 @@ class CollectionAPI {
@SubscribeEvent
fun onItemAdd(event: ItemAddInInventoryEvent) {
- // TODO add support for replenish (higher collection than actual items in inv)
val internalName = event.internalName
+ val (_, amount) = NEUItems.getMultiplier(internalName)
+ if (amount > 1) return
+
+ // TODO add support for replenish (higher collection than actual items in inv)
if (internalName.getItemStackOrNull() == null) {
LorenzUtils.debug("CollectionAPI.addFromInventory: item is null for '$internalName'")
return
diff --git a/src/main/java/at/hannibal2/skyhanni/data/MinecraftData.kt b/src/main/java/at/hannibal2/skyhanni/data/MinecraftData.kt
index bfcc8df5f..56d565299 100644
--- a/src/main/java/at/hannibal2/skyhanni/data/MinecraftData.kt
+++ b/src/main/java/at/hannibal2/skyhanni/data/MinecraftData.kt
@@ -6,6 +6,7 @@ import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
import at.hannibal2.skyhanni.events.PacketEvent
import at.hannibal2.skyhanni.events.PlaySoundEvent
import at.hannibal2.skyhanni.events.ReceiveParticleEvent
+import at.hannibal2.skyhanni.utils.DelayedRun
import at.hannibal2.skyhanni.utils.InventoryUtils
import at.hannibal2.skyhanni.utils.ItemUtils.getInternalName
import at.hannibal2.skyhanni.utils.LorenzUtils
@@ -71,6 +72,7 @@ object MinecraftData {
Minecraft.getMinecraft().thePlayer ?: return
totalTicks++
LorenzTickEvent(totalTicks).postAndCatch()
+ DelayedRun.checkRuns()
}
@SubscribeEvent
@@ -78,8 +80,8 @@ object MinecraftData {
if (!LorenzUtils.inSkyBlock) return
val hand = InventoryUtils.getItemInHand()
val newItem = hand?.getInternalName() ?: NEUInternalName.NONE
- if (newItem != InventoryUtils.itemInHandId) {
- ItemInHandChangeEvent(newItem, hand).postAndCatch()
+ val oldItem = InventoryUtils.itemInHandId
+ if (newItem != oldItem) {
InventoryUtils.recentItemsInHand.keys.removeIf { it + 30_000 > System.currentTimeMillis() }
if (newItem != NEUInternalName.NONE) {
@@ -87,6 +89,7 @@ object MinecraftData {
}
InventoryUtils.itemInHandId = newItem
InventoryUtils.latestItemInHand = hand
+ ItemInHandChangeEvent(newItem, oldItem).postAndCatch()
}
}
diff --git a/src/main/java/at/hannibal2/skyhanni/data/OwnInventoryData.kt b/src/main/java/at/hannibal2/skyhanni/data/OwnInventoryData.kt
index 4c6058b26..f3c04a0c6 100644
--- a/src/main/java/at/hannibal2/skyhanni/data/OwnInventoryData.kt
+++ b/src/main/java/at/hannibal2/skyhanni/data/OwnInventoryData.kt
@@ -1,23 +1,28 @@
package at.hannibal2.skyhanni.data
-import at.hannibal2.skyhanni.events.InventoryCloseEvent
+import at.hannibal2.skyhanni.events.GuiContainerEvent
import at.hannibal2.skyhanni.events.LorenzTickEvent
import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
import at.hannibal2.skyhanni.events.OwnInventoryItemUpdateEvent
import at.hannibal2.skyhanni.events.PacketEvent
import at.hannibal2.skyhanni.events.entity.ItemAddInInventoryEvent
import at.hannibal2.skyhanni.features.bazaar.BazaarApi
+import at.hannibal2.skyhanni.features.bazaar.BazaarApi.Companion.isBazaarItem
import at.hannibal2.skyhanni.utils.InventoryUtils
import at.hannibal2.skyhanni.utils.ItemUtils.getInternalNameOrNull
import at.hannibal2.skyhanni.utils.ItemUtils.name
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.LorenzUtils.editCopy
-import at.hannibal2.skyhanni.utils.NEUItems
+import at.hannibal2.skyhanni.utils.NEUInternalName
+import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import net.minecraft.client.Minecraft
import net.minecraft.item.ItemStack
import net.minecraft.network.play.server.S0DPacketCollectItem
import net.minecraft.network.play.server.S2FPacketSetSlot
import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
typealias SlotNumber = Int
typealias ItemName = String
@@ -58,7 +63,12 @@ class OwnInventoryData {
val old = items[slot]
val new = itemStack.itemToPair()
if (old != new) {
- item(slot, new, itemStack)
+ itemStack?.let {
+ calculateDifference(slot, new, it)
+ }
+ items = items.editCopy {
+ this[slot] = new
+ }
}
}
}
@@ -78,59 +88,65 @@ class OwnInventoryData {
private fun ItemStack?.itemToPair(): ItemData = this?.let { (name ?: "null") to stackSize } ?: Pair("null", 0)
- private fun item(slot: SlotNumber, new: ItemData, itemStack: ItemStack?) {
+ private fun calculateDifference(slot: SlotNumber, new: ItemData, itemStack: ItemStack) {
val (oldItem, oldAmount) = items[slot] ?: Pair("null", 0)
val (name, amount) = new
if (name == oldItem) {
val diff = amount - oldAmount
- if (amount > oldAmount) {
- add(itemStack, diff)
+ if (diff > 0) {
+ addItem(itemStack, diff)
}
} else {
if (name != "null") {
- add(itemStack!!, amount)
+ addItem(itemStack, amount)
}
}
- items = items.editCopy {
- this[slot] = new
- }
}
@SubscribeEvent
- fun onInventoryClose(event: InventoryCloseEvent) {
- BazaarApi.inBazaarInventory = false
- lastClose = System.currentTimeMillis()
+ fun onInventoryClose(event: GuiContainerEvent.CloseWindowEvent) {
+ val item = Minecraft.getMinecraft().thePlayer.inventory.itemStack ?: return
+ val internalNameOrNull = item.getInternalNameOrNull() ?: return
+ ignoreItem(500.milliseconds) { it == internalNameOrNull }
}
- private var lastClose = 0L
+ @SubscribeEvent
+ fun onSlotClick(event: GuiContainerEvent.SlotClickEvent) {
+ if (BazaarApi.inBazaarInventory) {
+ ignoreItem(500.milliseconds) { it.isBazaarItem() }
+ }
+ }
+
+ private fun ignoreItem(duration: Duration, condition: (NEUInternalName) -> Boolean) {
+ ignoredItemsUntil.add(IgnoredItem(condition, SimpleTimeMark.now() + duration))
+ }
- private fun add(item_: ItemStack?, add: Int) {
- val item = item_ ?: return
- val diffClose = System.currentTimeMillis() - lastClose
- if (diffClose < 500) return
+ private val ignoredItemsUntil = mutableListOf<IgnoredItem>()
+ class IgnoredItem(val condition: (NEUInternalName) -> Boolean, val blockedUntil: SimpleTimeMark)
+
+ private fun addItem(item: ItemStack, add: Int) {
val diffWorld = System.currentTimeMillis() - LorenzUtils.lastWorldSwitch
if (diffWorld < 3_000) return
- val internalName = item.getInternalNameOrNull()
-
item.name?.let {
if (it == "§8Quiver Arrow") {
return
}
}
- if (internalName == null) {
- LorenzUtils.debug("OwnInventoryData add is empty for: '${item.name}'")
+ val internalName = item.getInternalNameOrNull() ?: run {
+ LorenzUtils.debug("OwnInventoryData add is null for: '${item.name}'")
return
}
- if (internalName.startsWith("MAP-")) return
- val (_, amount) = NEUItems.getMultiplier(internalName)
- if (amount > 1) return
+ ignoredItemsUntil.removeIf { it.blockedUntil.isInPast() }
+ if (ignoredItemsUntil.any { it.condition(internalName) }) return
+
+ if (internalName.startsWith("MAP-")) return
ItemAddInInventoryEvent(internalName, add).postAndCatch()
+ LorenzUtils.debug("added item internalName: $internalName")
}
-
}
diff --git a/src/main/java/at/hannibal2/skyhanni/data/SkillExperience.kt b/src/main/java/at/hannibal2/skyhanni/data/SkillExperience.kt
index 78c25e799..470c8d171 100644
--- a/src/main/java/at/hannibal2/skyhanni/data/SkillExperience.kt
+++ b/src/main/java/at/hannibal2/skyhanni/data/SkillExperience.kt
@@ -3,6 +3,7 @@ package at.hannibal2.skyhanni.data
import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent
import at.hannibal2.skyhanni.events.LorenzActionBarEvent
import at.hannibal2.skyhanni.events.ProfileJoinEvent
+import at.hannibal2.skyhanni.events.SkillExpGainEvent
import at.hannibal2.skyhanni.utils.ItemUtils.getLore
import at.hannibal2.skyhanni.utils.ItemUtils.name
import at.hannibal2.skyhanni.utils.LorenzUtils
@@ -32,7 +33,14 @@ class SkillExperience {
val neededForNextLevel = group("needed").formatNumber()
val nextLevel = getLevelForExpExactly(neededForNextLevel)
val baseExp = getExpForLevel(nextLevel - 1)
- skillExp[skill] = baseExp + overflow
+ val totalExp = baseExp + overflow
+ skillExp[skill] = totalExp
+ SkillExpGainEvent(skill).postAndCatch()
+ }
+ val pattern = ".*§3+(?<add>.+) (?<skill>.*) \\((?<percentage>.*)%\\).*".toPattern()
+ pattern.matchMatcher(event.message) {
+ val skill = group("skill").lowercase()
+ SkillExpGainEvent(skill).postAndCatch()
}
}
diff --git a/src/main/java/at/hannibal2/skyhanni/events/ItemInHandChangeEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/ItemInHandChangeEvent.kt
index 3f5b94b3c..4ef39df43 100644
--- a/src/main/java/at/hannibal2/skyhanni/events/ItemInHandChangeEvent.kt
+++ b/src/main/java/at/hannibal2/skyhanni/events/ItemInHandChangeEvent.kt
@@ -1,6 +1,5 @@
package at.hannibal2.skyhanni.events
import at.hannibal2.skyhanni.utils.NEUInternalName
-import net.minecraft.item.ItemStack
-class ItemInHandChangeEvent(val internalName: NEUInternalName, val stack: ItemStack?) : LorenzEvent() \ No newline at end of file
+class ItemInHandChangeEvent(val newItem: NEUInternalName, val oldItem: NEUInternalName) : LorenzEvent()
diff --git a/src/main/java/at/hannibal2/skyhanni/events/SkillExpGainEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/SkillExpGainEvent.kt
new file mode 100644
index 000000000..c6c7be5cc
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/events/SkillExpGainEvent.kt
@@ -0,0 +1,4 @@
+package at.hannibal2.skyhanni.events
+
+// does not know how much exp is there, also gets called multiple times
+class SkillExpGainEvent(val skill: String) : LorenzEvent()
diff --git a/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingAPI.kt
index 035525c01..3224b286b 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingAPI.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingAPI.kt
@@ -35,7 +35,9 @@ object FishingAPI {
FishingBobberCastEvent(entity).postAndCatch()
}
- fun hasFishingRodInHand() = InventoryUtils.itemInHandId.asString().contains("ROD")
+ fun hasFishingRodInHand() = InventoryUtils.itemInHandId.isFishingRod()
+
+ fun NEUInternalName.isFishingRod() = contains("ROD")
fun ItemStack.isBait(): Boolean {
val name = name ?: return false
diff --git a/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingProfitTracker.kt b/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingProfitTracker.kt
index 50a9e6e1f..bbc5fd7f8 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingProfitTracker.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/fishing/FishingProfitTracker.kt
@@ -4,14 +4,18 @@ import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.events.EntityMoveEvent
import at.hannibal2.skyhanni.events.FishingBobberCastEvent
import at.hannibal2.skyhanni.events.GuiRenderEvent
+import at.hannibal2.skyhanni.events.ItemInHandChangeEvent
import at.hannibal2.skyhanni.events.LorenzChatEvent
import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
import at.hannibal2.skyhanni.events.RepositoryReloadEvent
import at.hannibal2.skyhanni.events.SackChangeEvent
+import at.hannibal2.skyhanni.events.SkillExpGainEvent
import at.hannibal2.skyhanni.events.entity.ItemAddInInventoryEvent
import at.hannibal2.skyhanni.features.bazaar.BazaarApi.Companion.getBazaarData
+import at.hannibal2.skyhanni.features.fishing.FishingAPI.isFishingRod
import at.hannibal2.skyhanni.test.PriceSource
-import at.hannibal2.skyhanni.utils.ItemUtils.getItemName
+import at.hannibal2.skyhanni.utils.DelayedRun
+import at.hannibal2.skyhanni.utils.ItemUtils.nameWithEnchantment
import at.hannibal2.skyhanni.utils.KeyboardManager
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.LorenzUtils.addAsSingletonList
@@ -19,6 +23,7 @@ import at.hannibal2.skyhanni.utils.LorenzUtils.addSelector
import at.hannibal2.skyhanni.utils.LorenzUtils.sortedDesc
import at.hannibal2.skyhanni.utils.NEUInternalName
import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName
+import at.hannibal2.skyhanni.utils.NEUItems.getItemStack
import at.hannibal2.skyhanni.utils.NEUItems.getNpcPriceOrNull
import at.hannibal2.skyhanni.utils.NEUItems.getPriceOrNull
import at.hannibal2.skyhanni.utils.NumberUtil
@@ -32,8 +37,11 @@ import at.hannibal2.skyhanni.utils.renderables.Renderable
import at.hannibal2.skyhanni.utils.tracker.SkyHanniTracker
import at.hannibal2.skyhanni.utils.tracker.TrackerData
import com.google.gson.annotations.Expose
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
import net.minecraft.client.Minecraft
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
object FishingProfitTracker {
@@ -42,6 +50,8 @@ object FishingProfitTracker {
private val coinsChatPattern = ".* CATCH! §r§bYou found §r§6(?<coins>.*) Coins§r§b\\.".toPattern()
private var lastClickDelay = 0L
+ private var lastFishingTime = SimpleTimeMark.farPast()
+
private val tracker =
SkyHanniTracker("Fishing Profit Tracker", { Data() }, { it.fishing.fishingProfitTracker }) { drawDisplay(it) }
@@ -71,11 +81,11 @@ object FishingProfitTracker {
var hidden = false
override fun toString() = "FishingItem{" +
- "internalName='" + internalName + '\'' +
- ", timesDropped=" + timesCaught +
- ", totalAmount=" + totalAmount +
- ", hidden=" + hidden +
- '}'
+ "internalName='" + internalName + '\'' +
+ ", timesDropped=" + timesCaught +
+ ", totalAmount=" + totalAmount +
+ ", hidden=" + hidden +
+ '}'
var lastTimeUpdated = SimpleTimeMark.farPast()
}
@@ -102,8 +112,9 @@ object FishingProfitTracker {
itemProfit.timesCaught
} else amount
- val cleanName = if (internalName == SKYBLOCK_COIN) "§6Coins" else internalName.getItemName()
- var name = cleanName
+ val cleanName =
+ if (internalName == SKYBLOCK_COIN) "§6Coins" else internalName.getItemStack().nameWithEnchantment
+ var name = cleanName ?: error("no name for $internalName")
val priceFormat = NumberUtil.format(price)
val hidden = itemProfit.hidden
@@ -211,7 +222,9 @@ object FishingProfitTracker {
fun onItemAdd(event: ItemAddInInventoryEvent) {
if (!isEnabled()) return
- maybeAddItem(event.internalName, event.amount)
+ DelayedRun.runDelayed(500.milliseconds) {
+ maybeAddItem(event.internalName, event.amount)
+ }
}
@SubscribeEvent
@@ -235,6 +248,28 @@ object FishingProfitTracker {
}
@SubscribeEvent
+ fun onItemInHandChange(event: ItemInHandChangeEvent) {
+ if (event.oldItem.isFishingRod()) {
+ lastFishingTime = SimpleTimeMark.now()
+ }
+ if (event.newItem.isFishingRod()) {
+ DelayedRun.runDelayed(1.seconds) {
+ lastFishingTime = SimpleTimeMark.now()
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onSkillExpGain(event: SkillExpGainEvent) {
+ val skill = event.skill
+ if (isEnabled()) {
+ if (skill != "fishing") {
+ lastFishingTime = SimpleTimeMark.farPast()
+ }
+ }
+ }
+
+ @SubscribeEvent
fun onRenderOverlay(event: GuiRenderEvent) {
if (!isEnabled()) return
if (!FishingAPI.hasFishingRodInHand()) return
@@ -244,6 +279,8 @@ object FishingProfitTracker {
}
private fun maybeAddItem(internalName: NEUInternalName, amount: Int) {
+ if (lastFishingTime.passedSince() > 10.minutes) return
+
if (!isAllowedItem(internalName)) {
LorenzUtils.debug("Ignored non-fishing item pickup: $internalName'")
return
@@ -252,23 +289,69 @@ object FishingProfitTracker {
addItem(internalName, amount)
}
- private var itemCategories = mutableMapOf<String, List<NEUInternalName>>()
+ private var itemCategories = mapOf<String, List<NEUInternalName>>()
+
+ private var shItemCategories = mapOf<String, List<NEUInternalName>>()
+ private var neuItemCategories = mapOf<String, List<NEUInternalName>>()
@SubscribeEvent
fun onRepoReload(event: RepositoryReloadEvent) {
- itemCategories = event.getConstant<FishingProfitItemsJson>("FishingProfitItems").categories
+ shItemCategories = event.getConstant<FishingProfitItemsJson>("FishingProfitItems").categories
+ updateItemCategories()
}
- private fun isAllowedItem(internalName: NEUInternalName): Boolean {
- for ((name, items) in itemCategories) {
- if (internalName in items) {
- return true
+ private fun updateItemCategories() {
+ itemCategories = shItemCategories + neuItemCategories
+ }
+
+ @SubscribeEvent
+ fun onNeuRepoReload(event: io.github.moulberry.notenoughupdates.events.RepositoryReloadEvent) {
+ val totalDrops = mutableListOf<String>()
+ val dropCategories = mutableMapOf<String, MutableList<NEUInternalName>>()
+ for ((seaCreature, data) in NotEnoughUpdates.INSTANCE.manager.itemInformation.filter { it.key.endsWith("_SC") }) {
+ val asJsonObject = data.getAsJsonArray("recipes")[0].asJsonObject
+ val drops = asJsonObject.getAsJsonArray("drops")
+ .map { it.asJsonObject.get("id").asString }.map { it.split(":").first() }
+ val asJsonArray = asJsonObject.get("extra")
+ val extra = asJsonArray?.let {
+ asJsonArray.asJsonArray.toList()
+ .map { it.toString() }
+ .filter { !it.contains("Fishing Skill") && !it.contains("Requirements:") && !it.contains("Fished from water") }
+ .joinToString(" + ")
+ } ?: "null"
+ val category = if (extra.contains("Fishing Festival")) {
+ "Fishing Festival"
+ } else if (extra.contains("Spooky Festival")) {
+ "Spooky Festival"
+ } else if (extra.contains("Jerry's Workshop")) {
+ "Jerry's Workshop"
+ } else if (extra.contains("Oasis")) {
+ "Oasis"
+ } else if (extra.contains("Magma Fields") || extra.contains("Precursor Remnants") ||
+ extra.contains("Goblin Holdout")
+ ) {
+ "Crystal Hollows"
+ } else if (extra.contains("Crimson Isle Lava")) {
+ "Crimson Isle Lava"
+ } else {
+ if (extra.isNotEmpty()) {
+ println("unknown extra: $extra = $seaCreature ($drops)")
+ }
+ "Water"
+ } + " Sea Creatures"
+ for (drop in drops) {
+ if (drop !in totalDrops) {
+ totalDrops.add(drop)
+ dropCategories.getOrPut(category) { mutableListOf() }.add(drop.asInternalName())
+ }
}
}
-
- return false
+ neuItemCategories = dropCategories
+ updateItemCategories()
}
+ private fun isAllowedItem(internalName: NEUInternalName) = itemCategories.any { internalName in it.value }
+
private fun getPrice(internalName: NEUInternalName) = when (config.priceFrom) {
0 -> internalName.getBazaarData()?.sellPrice ?: internalName.getPriceOrNull() ?: 0.0
1 -> internalName.getBazaarData()?.buyPrice ?: internalName.getPriceOrNull() ?: 0.0
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/DelayedRun.kt b/src/main/java/at/hannibal2/skyhanni/utils/DelayedRun.kt
new file mode 100644
index 000000000..0fa9152cd
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/DelayedRun.kt
@@ -0,0 +1,21 @@
+package at.hannibal2.skyhanni.utils
+
+import kotlin.time.Duration
+
+object DelayedRun {
+ val map = mutableMapOf<() -> Any, SimpleTimeMark>()
+
+ fun runDelayed(duration: Duration, run: () -> Unit) {
+ map[run] = SimpleTimeMark.now() + duration
+ }
+
+ fun checkRuns() {
+ map.entries.removeIf { (runnable, time) ->
+ val inPast = time.isInPast()
+ if (inPast) {
+ runnable()
+ }
+ inPast
+ }
+ }
+}