diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/misc/MiscConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/misc/MiscConfig.java index cf75a3411bc3..05b11c24114e 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/misc/MiscConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/misc/MiscConfig.java @@ -8,6 +8,7 @@ import at.hannibal2.skyhanni.config.features.minion.MinionsConfig; import at.hannibal2.skyhanni.config.features.misc.pets.PetConfig; import at.hannibal2.skyhanni.config.features.stranded.StrandedConfig; +import at.hannibal2.skyhanni.features.misc.EnchantedClockHelper; import com.google.gson.annotations.Expose; import io.github.notenoughupdates.moulconfig.annotations.Accordion; import io.github.notenoughupdates.moulconfig.annotations.Category; @@ -359,4 +360,9 @@ public class MiscConfig { @ConfigEditorBoolean @FeatureToggle public boolean warnAboutPcTimeOffset = true; + + @Expose + @ConfigOption(name = "Enchanted Clock Reminder", desc = "Show reminders when an Enchanted Clock charge for a boost type is available.") + @ConfigEditorDraggableList + public List enchantedClockReminder = new ArrayList<>(); } 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 44e4fd6dfd65..0d0984d20b15 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -39,12 +39,14 @@ import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseTracker; import at.hannibal2.skyhanni.features.mining.powdertracker.PowderTracker; import at.hannibal2.skyhanni.features.misc.DraconicSacrificeTracker; +import at.hannibal2.skyhanni.features.misc.EnchantedClockHelper; import at.hannibal2.skyhanni.features.misc.trevor.TrevorTracker; import at.hannibal2.skyhanni.features.rift.area.westvillage.VerminTracker; import at.hannibal2.skyhanni.features.rift.area.westvillage.kloon.KloonTerminal; import at.hannibal2.skyhanni.features.skillprogress.SkillType; import at.hannibal2.skyhanni.features.slayer.SlayerProfitTracker; import at.hannibal2.skyhanni.utils.GenericWrapper; +import at.hannibal2.skyhanni.utils.LorenzColor; import at.hannibal2.skyhanni.utils.LorenzRarity; import at.hannibal2.skyhanni.utils.LorenzVec; import at.hannibal2.skyhanni.utils.NEUInternalName; @@ -885,4 +887,49 @@ public int hashCode() { ); } } + + @Expose + public EnchantedClockStats enchantedClockStats = new EnchantedClockStats(); + + public static class EnchantedClockStats { + @Expose + public Map clockBoosts = new HashMap<>(); + + public static class ClockBoostStatus { + @Expose + public ClockBoostState state; + + @Expose + @Nullable + public SimpleTimeMark availableAt; + + @Expose + public boolean warned = false; + + public enum ClockBoostState { + READY("Ready", LorenzColor.GREEN), + CHARGING("Charging", LorenzColor.RED), + PROBLEM("Problem", LorenzColor.YELLOW), + ; + + public final String displayName; + public final LorenzColor color; + + ClockBoostState(String displayName, LorenzColor color) { + this.displayName = displayName; + this.color = color; + } + + @Override + public String toString() { + return "§" + color.getChatColorCode() + displayName; + } + } + + public ClockBoostStatus(ClockBoostState state, @Nullable SimpleTimeMark availableAt) { + this.state = state; + this.availableAt = availableAt; + } + } + } } diff --git a/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/EnchantedClockJson.kt b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/EnchantedClockJson.kt new file mode 100644 index 000000000000..68ed492a60d8 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/EnchantedClockJson.kt @@ -0,0 +1,17 @@ +package at.hannibal2.skyhanni.data.jsonobjects.repo + +import com.google.gson.annotations.Expose + +data class EnchantedClockJson( + @Expose val boosts: List +) + +data class BoostJson( + @Expose val name: String, + @Expose val displayName: String, + @Expose val usageString: String?, + @Expose val color: String, + @Expose val displaySlot: Int, + @Expose val statusSlot: Int, + @Expose val cooldownHours: Int = 48, +) diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt index da6277e185b2..77161eb1aa51 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt @@ -109,7 +109,7 @@ object ChocolateFactoryStats { put(ChocolateFactoryStat.LEADERBOARD_POS, "§ePosition: §b$leaderboard") } - private fun SimpleTimeMark?.formatIfFuture(): String? = this?.takeIfFuture()?.timeUntil()?.format() + private fun SimpleTimeMark?.formatIfFuture(): String? = this?.takeIf { it.isInFuture() }?.timeUntil()?.format() private fun MutableMap.addHitman() { val profileStorage = ChocolateFactoryStats.profileStorage ?: return diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt index 358ee25e5e15..75cb4f9c24d6 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt @@ -142,7 +142,9 @@ object HitmanAPI { * menu, and only gives cooldown timers... */ fun HitmanStatsStorage.getOpenSlots(): Int { - val allSlotsCooldownDuration = allSlotsCooldownMark?.takeIfFuture()?.timeUntil() ?: return getPurchasedSlots() + val allSlotsCooldownDuration = allSlotsCooldownMark?.takeIf { + it.isInFuture() + }?.timeUntil() ?: return getPurchasedSlots() val slotsOnCooldown = ceil(allSlotsCooldownDuration.inPartialMinutes / MINUTES_PER_DAY).toInt() return getPurchasedSlots() - slotsOnCooldown - getAvailableEggs() } diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/EnchantedClockHelper.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/EnchantedClockHelper.kt new file mode 100644 index 000000000000..926c8f49c43b --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/misc/EnchantedClockHelper.kt @@ -0,0 +1,237 @@ +package at.hannibal2.skyhanni.features.misc + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage +import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage.EnchantedClockStats.ClockBoostStatus.ClockBoostState +import at.hannibal2.skyhanni.data.ProfileStorageData +import at.hannibal2.skyhanni.data.jsonobjects.repo.EnchantedClockJson +import at.hannibal2.skyhanni.events.InventoryUpdatedEvent +import at.hannibal2.skyhanni.events.LorenzChatEvent +import at.hannibal2.skyhanni.events.RepositoryReloadEvent +import at.hannibal2.skyhanni.events.SecondPassedEvent +import at.hannibal2.skyhanni.features.misc.EnchantedClockHelper.ClockBoostType.Companion.filterStatusSlots +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.RegexUtils.firstMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matches +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import net.minecraft.item.ItemStack +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours + +@SkyHanniModule +object EnchantedClockHelper { + + private val patternGroup = RepoPattern.group("misc.eclock") + private val storage get() = ProfileStorageData.profileSpecific?.enchantedClockStats + private val config get() = SkyHanniMod.feature.misc + + // + /** + * REGEX-TEST: Enchanted Time Clock + */ + private val enchantedClockPattern by patternGroup.pattern( + "inventory.name", + "Enchanted Time Clock", + ) + + /** + * REGEX-TEST: §6§lTIME WARP! §r§aYou have successfully warped time for your Chocolate Factory! + * REGEX-TEST: §6§lTIME WARP! §r§aYou have successfully warped time for your minions! + * REGEX-TEST: §6§lTIME WARP! §r§aYou have successfully warped time for your forges! + * REGEX-TEST: §6§lTIME WARP! §r§aYou have successfully warped time for your aging items! + * REGEX-TEST: §6§lTIME WARP! §r§aYou have successfully warped time for your training pets! + * REGEX-TEST: §6§lTIME WARP! §r§aYou have successfully warped time for your pets being taken care of by Kat! + */ + private val boostUsedChatPattern by patternGroup.pattern( + "chat.boostused", + "§6§lTIME WARP! §r§aYou have successfully warped time for your (?.+?)!", + ) + + /** + * REGEX-TEST: §7Status: §c§lCHARGING + * REGEX-TEST: §7Status: §e§lPROBLEM + * REGEX-TEST: §7Status: §a§lREADY + */ + private val statusLorePattern by patternGroup.pattern( + "inventory.status", + "§7Status: §(?[a-f])§l(?.+)", + ) + + /** + * REGEX-TEST: §7§cOn cooldown: 20 hours + */ + private val cooldownLorePattern by patternGroup.pattern( + "inventory.cooldown", + "(?:§.)*On cooldown: (?\\d+) hours?", + ) + // + + enum class SimpleClockBoostType( + val displayString: String + ) { + MINIONS("§bMinions"), + CHOCOLATE_FACTORY("§6Chocolate Factory"), + PET_TRAINING("§dPet Training"), + PET_SITTER("§bPet Sitter"), + AGING_ITEMS("§eAging Items"), + FORGE("§6Forge"), + ; + + override fun toString(): String = displayString + } + + data class ClockBoostType( + val name: String, + val displayName: String, + val usageString: String, + val color: LorenzColor, + val displaySlot: Int, + val statusSlot: Int, + val cooldown: Duration = 48.hours + ) { + val formattedName: String = "§${color.chatColorCode}$displayName" + + fun getCooldownFromNow() = SimpleTimeMark.now() + cooldown + + fun toSimple(): SimpleClockBoostType? { + return try { + SimpleClockBoostType.valueOf(name.uppercase()) + } catch (e: IllegalArgumentException) { + null + } + } + + companion object { + private val entries = mutableListOf() + + fun clear() = entries.clear() + + fun populateFromJson(json: EnchantedClockJson) { + entries.clear() + entries.addAll( + json.boosts.map { boost -> + ClockBoostType( + name = boost.name, + displayName = boost.displayName, + usageString = boost.usageString ?: boost.displayName, + color = LorenzColor.valueOf(boost.color), + displaySlot = boost.displaySlot, + statusSlot = boost.statusSlot, + cooldown = boost.cooldownHours.hours + ) + } + ) + } + + fun byUsageStringOrNull(usageString: String) = entries.firstOrNull { it.usageString == usageString } + + fun byItemStackOrNull(stack: ItemStack) = entries.firstOrNull { it.formattedName == stack.displayName } + + fun fromSimple(simple: SimpleClockBoostType) = entries.firstOrNull { it.displayName == simple.displayString } + + fun Map.filterStatusSlots() = filterKeys { key -> + ClockBoostType.entries.any { entry -> + entry.statusSlot == key + } + } + } + } + + @SubscribeEvent + fun onSecondPassed(event: SecondPassedEvent) { + val storage = storage ?: return + + val readyNowBoosts: MutableList = mutableListOf() + + for ((boostType, status) in storage.clockBoosts.filter { !it.value.warned }) { + val inConfig = boostType != null && config.enchantedClockReminder.contains(boostType) + val isProperState = status.state == ClockBoostState.CHARGING + val inFuture = status.availableAt?.isInFuture() == true + if (!inConfig || !isProperState || inFuture) continue + + val complexType = ClockBoostType.fromSimple(boostType) ?: continue + + status.state = ClockBoostState.READY + status.availableAt = null + status.warned = true + readyNowBoosts.add(complexType) + } + + if (readyNowBoosts.isEmpty()) return + val boostList = readyNowBoosts.joinToString(", ") { it.formattedName } + val starter = if (readyNowBoosts.size == 1) "boost is ready" else "boosts are ready" + ChatUtils.chat("§6§lTIME WARP! §r§aThe following $starter:\n$boostList") + } + + @SubscribeEvent + fun onRepoReload(event: RepositoryReloadEvent) { + val data = event.getConstant("EnchantedClock") + ClockBoostType.clear() + ClockBoostType.populateFromJson(data) + } + + @SubscribeEvent + fun onChat(event: LorenzChatEvent) { + boostUsedChatPattern.matchMatcher(event.message) { + val usageString = group("usagestring") ?: return@matchMatcher + val boostType = ClockBoostType.byUsageStringOrNull(usageString) ?: return@matchMatcher + val simpleType = boostType.toSimple() ?: return@matchMatcher + val storage = storage ?: return@matchMatcher + storage.clockBoosts.getOrPut(simpleType) { + ProfileSpecificStorage.EnchantedClockStats.ClockBoostStatus( + ClockBoostState.CHARGING, + boostType.getCooldownFromNow() + ) + } + } + } + + @SubscribeEvent + fun onInventoryUpdatedEvent(event: InventoryUpdatedEvent) { + if (!enchantedClockPattern.matches(event.inventoryName)) return + val storage = storage ?: return + + val statusStacks = event.inventoryItems.filterStatusSlots() + for ((_, stack) in statusStacks) { + val boostType = ClockBoostType.byItemStackOrNull(stack) ?: continue + val simpleType = boostType.toSimple() ?: continue + + val currentStatus: ClockBoostState = statusLorePattern.firstMatcher(stack.getLore()) { + group("status")?.let { statusStr -> + runCatching { ClockBoostState.valueOf(statusStr) }.getOrElse { + ErrorManager.skyHanniError("Invalid status string: $statusStr") + } + } + } ?: continue + + val parsedCooldown: SimpleTimeMark? = when (currentStatus) { + ClockBoostState.READY, ClockBoostState.PROBLEM -> null + else -> cooldownLorePattern.firstMatcher(stack.getLore()) { + group("hours")?.toIntOrNull()?.hours?.let { SimpleTimeMark.now() + it } + } + } + + // Because the times provided by the clock UI suck ass (we only get hour count) + // We only want to set it if the current time is horribly incorrect. + storage.clockBoosts[simpleType]?.availableAt?.let { existing -> + parsedCooldown?.let { parsed -> + if (existing.absoluteDifference(parsed) < 2.hours) return + } + } + + storage.clockBoosts.getOrPut(simpleType) { + ProfileSpecificStorage.EnchantedClockStats.ClockBoostStatus( + currentStatus, + parsedCooldown + ) + } + } + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/SimpleTimeMark.kt b/src/main/java/at/hannibal2/skyhanni/utils/SimpleTimeMark.kt index 35437595168a..333aa612ef7c 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/SimpleTimeMark.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/SimpleTimeMark.kt @@ -5,6 +5,7 @@ import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter +import kotlin.math.abs import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -31,11 +32,9 @@ value class SimpleTimeMark(private val millis: Long) : Comparable