Skip to content

Commit

Permalink
Feature: Enchanted Clock Reminders (#3051)
Browse files Browse the repository at this point in the history
Co-authored-by: CalMWolfs <[email protected]>
Co-authored-by: hannibal2 <[email protected]>
Co-authored-by: calwolfson <[email protected]>
  • Loading branch information
4 people authored Jan 9, 2025
1 parent 3e2ca93 commit 7fd3e99
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package at.hannibal2.skyhanni.config.features.misc;

import at.hannibal2.skyhanni.config.FeatureToggle;
import at.hannibal2.skyhanni.features.misc.EnchantedClockHelper;
import com.google.gson.annotations.Expose;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDraggableList;
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorSlider;
import io.github.notenoughupdates.moulconfig.annotations.ConfigOption;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class EnchantedClockConfig {

@Expose
@ConfigOption(name = "Enchanted Clock Reminder", desc = "Show reminders when an Enchanted Clock charge for a boost type is available.")
@ConfigEditorBoolean
@FeatureToggle
public boolean reminder = true;

@Expose
@ConfigOption(name = "Reminder Boosts", desc = "List of boost types to remind about.")
@ConfigEditorDraggableList
public List<EnchantedClockHelper.SimpleBoostType> reminderBoosts = new ArrayList<>(Arrays.asList(
EnchantedClockHelper.SimpleBoostType.MINIONS,
EnchantedClockHelper.SimpleBoostType.CHOCOLATE_FACTORY,
EnchantedClockHelper.SimpleBoostType.PET_TRAINING,
EnchantedClockHelper.SimpleBoostType.PET_SITTER,
EnchantedClockHelper.SimpleBoostType.AGING_ITEMS,
EnchantedClockHelper.SimpleBoostType.FORGE)
);

@Expose
@ConfigOption(
name = "Repeat Reminder",
desc = "Repeat reminders every §cX §7minutes until you use the boost.\n" +
"§eSet to 0 to disable."
)
@ConfigEditorSlider(minValue = 0, maxValue = 60, minStep = 1)
public int repeatReminder = 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public class MiscConfig {
@Accordion
public LastServersConfig lastServers = new LastServersConfig();

@Expose
@ConfigOption(name = "Enchanted Clock", desc = "")
@Accordion
public EnchantedClockConfig enchantedClock = new EnchantedClockConfig();

@Expose
@ConfigOption(name = "Reset Search on Close", desc = "Reset the search in GUIs after closing the inventory.")
@ConfigEditorBoolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import at.hannibal2.skyhanni.features.mining.fossilexcavator.ExcavatorProfitTrac
import at.hannibal2.skyhanni.features.mining.glacitemineshaft.CorpseTracker.BucketData
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.TrapperMobRarity
import at.hannibal2.skyhanni.features.rift.area.westvillage.VerminTracker
import at.hannibal2.skyhanni.features.rift.area.westvillage.kloon.KloonTerminal
Expand Down Expand Up @@ -790,6 +791,7 @@ class ProfileSpecificStorage {
override fun hashCode(): Int {
return Objects.hash(position, percentile)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null) return false
Expand Down Expand Up @@ -833,4 +835,8 @@ class ProfileSpecificStorage {
return otherProp.hashCode() == hashCode()
}
}

@Expose
var enchantedClockBoosts: MutableMap<EnchantedClockHelper.SimpleBoostType, EnchantedClockHelper.Status> =
EnumMap(EnchantedClockHelper.SimpleBoostType::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package at.hannibal2.skyhanni.data.jsonobjects.repo

import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

data class EnchantedClockJson(
@Expose val boosts: List<BoostJson>,
)

data class BoostJson(
@Expose val name: String,
@Expose @SerializedName("display_name") val displayName: String,
@Expose @SerializedName("usage_string") val usageString: String?,
@Expose val color: String,
@Expose @SerializedName("display_slot") val displaySlot: Int,
@Expose @SerializedName("status_slot") val statusSlot: Int,
@Expose @SerializedName("usage_hours") val cooldownHours: Int = 48,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import at.hannibal2.skyhanni.data.ProfileStorageData
import at.hannibal2.skyhanni.events.GuiContainerEvent
import at.hannibal2.skyhanni.events.MessageSendToServerEvent
import at.hannibal2.skyhanni.features.event.hoppity.MythicRabbitPetWarning
import at.hannibal2.skyhanni.features.misc.EnchantedClockHelper
import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule
import at.hannibal2.skyhanni.utils.ChatUtils
import at.hannibal2.skyhanni.utils.HypixelCommands
import at.hannibal2.skyhanni.utils.InventoryUtils
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.LorenzVec
import at.hannibal2.skyhanni.utils.RegexUtils.matches
Expand Down Expand Up @@ -53,6 +55,7 @@ object ChocolateFactoryBlockOpen {
if (!LorenzUtils.inSkyBlock) return
val slotDisplayName = event.slot?.stack?.displayName ?: return
if (!openCfItemPattern.matches(slotDisplayName)) return
if (EnchantedClockHelper.enchantedClockPattern.matches(InventoryUtils.openInventoryName())) return

if (checkIsBlocked()) event.cancel()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,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<ChocolateFactoryStat, String>.addHitman() {
val profileStorage = ChocolateFactoryStats.profileStorage ?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ object HitmanAPI {
* menu, and only gives cooldown timers...
*/
fun HitmanStatsStorage.getOpenSlots(): Int {
val allSlotsCooldownDuration = allSlotsCooldownMark?.takeIfFuture()?.timeUntil() ?: return purchasedHitmanSlots
val allSlotsCooldownDuration = allSlotsCooldownMark?.takeIf {
it.isInFuture()
}?.timeUntil() ?: return purchasedHitmanSlots
val slotsOnCooldown = ceil(allSlotsCooldownDuration.inPartialMinutes / MINUTES_PER_DAY).toInt()
return purchasedHitmanSlots - slotsOnCooldown - availableHitmanEggs
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package at.hannibal2.skyhanni.features.misc

import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.api.event.HandleEvent
import at.hannibal2.skyhanni.data.ProfileStorageData
import at.hannibal2.skyhanni.data.jsonobjects.repo.EnchantedClockJson
import at.hannibal2.skyhanni.events.InventoryOpenEvent
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.BoostType.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.DelayedRun
import at.hannibal2.skyhanni.utils.ItemUtils.getLore
import at.hannibal2.skyhanni.utils.LorenzColor
import at.hannibal2.skyhanni.utils.LorenzUtils
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.SoundUtils
import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
import com.google.gson.annotations.Expose
import net.minecraft.item.ItemStack
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes

@SkyHanniModule
object EnchantedClockHelper {

private val patternGroup = RepoPattern.group("misc.eclock")
private val storage get() = ProfileStorageData.profileSpecific?.enchantedClockBoosts
private val config get() = SkyHanniMod.feature.misc.enchantedClock

// <editor-fold desc="Patterns">
/**
* REGEX-TEST: Enchanted Time Clock
*/
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 (?<usagestring>.+?)!",
)

/**
* REGEX-TEST: §7Status: §c§lCHARGING
* REGEX-TEST: §7Status: §e§lPROBLEM
* REGEX-TEST: §7Status: §a§lREADY
*/
private val statusLorePattern by patternGroup.pattern(
"inventory.status",
"§7Status: §(?<color>[a-f])§l(?<status>.+)",
)

/**
* REGEX-TEST: §7§cOn cooldown: 20 hours
*/
private val cooldownLorePattern by patternGroup.pattern(
"inventory.cooldown",
"(?:§.)*On cooldown: (?<hours>\\d+) hours?",
)
// </editor-fold>

enum class SimpleBoostType(private 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 BoostType(
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(): SimpleBoostType? = SimpleBoostType.entries.find { it.name == name }

companion object {
private var entries = listOf<BoostType>()

fun byUsageStringOrNull(usageString: String) = entries.firstOrNull { it.usageString == usageString }
fun byItemStackOrNull(stack: ItemStack) = entries.firstOrNull { it.formattedName == stack.displayName }
fun bySimpleBoostType(simple: SimpleBoostType) = entries.firstOrNull { it.name == simple.name }

fun populateFromJson(json: EnchantedClockJson) {
entries = json.boosts.map {
BoostType(
name = it.name,
displayName = it.displayName,
usageString = it.usageString ?: it.displayName,
color = LorenzColor.valueOf(it.color),
displaySlot = it.displaySlot,
statusSlot = it.statusSlot,
cooldown = it.cooldownHours.hours,
)
}
}

fun Map<Int, ItemStack>.filterStatusSlots() = filterKeys { key ->
BoostType.entries.any { entry ->
entry.statusSlot == key
}
}
}
}

@SubscribeEvent
fun onSecondPassed(event: SecondPassedEvent) {
if (!isEnabled()) return
val readyNowBoosts = loadBoostsReadyNow().takeIf { it.isNotEmpty() } ?: return

val boostListFormat = readyNowBoosts.joinToString(", ") { it.formattedName }
val preamble = if (readyNowBoosts.size == 1) "boost is ready" else "boosts are ready"
ChatUtils.chat("§6§lTIME WARP! §r§aThe following $preamble:\n$boostListFormat")
SoundUtils.playPlingSound()

// Set up repeating reminder if enabled in config
config.repeatReminder.takeIf { it > 0 }?.let { interval ->
val simpleBoostsReadyNow = readyNowBoosts.mapNotNull { it.toSimple() }
DelayedRun.runDelayed(interval.minutes) {
storage?.filterKeys { it in simpleBoostsReadyNow }?.values?.forEach { it.warned = false }
}
}
}

private fun loadBoostsReadyNow(): List<BoostType> {
val storage = EnchantedClockHelper.storage ?: return emptyList()

val readyNowBoosts: MutableList<BoostType> = mutableListOf()

for ((type, status) in storage.filter { !it.value.warned }) {
val inConfig = config.reminderBoosts.contains(type)
val isProperState = status.state == State.CHARGING
val inFuture = status.availableAt?.isInFuture() == true
if (!inConfig || !isProperState || inFuture) continue

val complexType = BoostType.bySimpleBoostType(type) ?: continue

status.state = State.READY
status.availableAt = null
status.warned = true
readyNowBoosts.add(complexType)
}
return readyNowBoosts
}

@HandleEvent
fun onRepoReload(event: RepositoryReloadEvent) {
val data = event.getConstant<EnchantedClockJson>("misc/EnchantedClock")
BoostType.populateFromJson(data)
}

@SubscribeEvent
fun onChat(event: LorenzChatEvent) {
val usageString = boostUsedChatPattern.matchMatcher(event.message) { group("usagestring") } ?: return
val boostType = BoostType.byUsageStringOrNull(usageString) ?: return
val simpleType = boostType.toSimple() ?: return
val storage = storage ?: return
storage[simpleType] = Status(State.CHARGING, boostType.getCooldownFromNow())
}

private fun ItemStack.getTypePair(): Pair<BoostType?, SimpleBoostType?> {
val boostType = BoostType.byItemStackOrNull(this) ?: return null to null
val simpleType = boostType.toSimple() ?: return null to null
return boostType to simpleType
}

private fun ItemStack.getBoostState(): State? = statusLorePattern.firstMatcher(getLore()) {
group("status")?.let { statusStr ->
runCatching { State.valueOf(statusStr) }.getOrElse {
ErrorManager.skyHanniError("Invalid status string: $statusStr")
}
}
}

@HandleEvent
fun onInventoryOpen(event: InventoryOpenEvent) {
if (!enchantedClockPattern.matches(event.inventoryName)) return
val storage = storage ?: return

val statusStacks = event.inventoryItems.filterStatusSlots()
for ((_, stack) in statusStacks) {
val (boostType, simpleType) = stack.getTypePair()
val currentBoostState = stack.getBoostState()
if (boostType == null || simpleType == null || currentBoostState == null) continue

val parsedCooldown: SimpleTimeMark? = when (currentBoostState) {
State.READY, State.PROBLEM -> {
storage[simpleType]?.availableAt = SimpleTimeMark.now()
continue
}

else -> cooldownLorePattern.firstMatcher(stack.getLore()) {
group("hours")?.toIntOrNull()?.hours?.let { SimpleTimeMark.now() + it }
}
}

// Because the times provided by the clock UI is inaccurate (we only get hour count)
// We only want to set it if the current time is horribly incorrect.
storage[simpleType]?.availableAt?.let { existing ->
parsedCooldown?.let { parsed ->
if (existing.absoluteDifference(parsed) < 2.hours) return
}
}

storage[simpleType] = Status(currentBoostState, parsedCooldown)
}
}

class Status(
@field:Expose var state: State,
@field:Expose var availableAt: SimpleTimeMark?,
@field:Expose var warned: Boolean = false,
) {
override fun toString(): String = "Status(state=$state, availableAt=$availableAt, warned=$warned)"
}

enum class State(val displayName: String, val color: LorenzColor) {
READY("Ready", LorenzColor.GREEN),
CHARGING("Charging", LorenzColor.RED),
PROBLEM("Problem", LorenzColor.YELLOW),
;

override fun toString(): String = "§" + color.chatColorCode + displayName
}

fun isEnabled() = LorenzUtils.inSkyBlock && config.reminder
}
Loading

0 comments on commit 7fd3e99

Please sign in to comment.