Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Enchanted Clock Reminders #3051

Open
wants to merge 13 commits into
base: beta
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<EnchantedClockHelper.SimpleClockBoostType> enchantedClockReminder = new ArrayList<>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -885,4 +887,49 @@ public int hashCode() {
);
}
}

@Expose
public EnchantedClockStats enchantedClockStats = new EnchantedClockStats();

public static class EnchantedClockStats {
@Expose
public Map<EnchantedClockHelper.SimpleClockBoostType, ClockBoostStatus> 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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package at.hannibal2.skyhanni.data.jsonobjects.repo

import com.google.gson.annotations.Expose

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

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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChocolateFactoryStat, String>.addHitman() {
val profileStorage = ChocolateFactoryStats.profileStorage ?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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

// <editor-fold desc="Patterns">
/**
* 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 (?<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 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<ClockBoostType>()

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<Int, ItemStack>.filterStatusSlots() = filterKeys { key ->
ClockBoostType.entries.any { entry ->
entry.statusSlot == key
}
}
}
}

@SubscribeEvent
fun onSecondPassed(event: SecondPassedEvent) {
val storage = storage ?: return

val readyNowBoosts: MutableList<ClockBoostType> = 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<EnchantedClockJson>("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
)
}
}
}
}
9 changes: 3 additions & 6 deletions src/main/java/at/hannibal2/skyhanni/utils/SimpleTimeMark.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -31,11 +32,9 @@ value class SimpleTimeMark(private val millis: Long) : Comparable<SimpleTimeMark

fun isFarFuture() = millis == Long.MAX_VALUE

fun isFarPastOrFuture() = isFarPast() || isFarFuture()
fun takeIfInitialized() = if (isFarPast() || isFarFuture()) null else this

fun takeIfInitialized() = if (isFarPastOrFuture()) null else this

fun takeIfFuture() = if (isInFuture()) this else null
fun absoluteDifference(other: SimpleTimeMark) = abs(millis - other.millis).milliseconds

override fun compareTo(other: SimpleTimeMark): Int = millis.compareTo(other.millis)

Expand Down Expand Up @@ -64,8 +63,6 @@ value class SimpleTimeMark(private val millis: Long) : Comparable<SimpleTimeMark

fun toSkyBlockTime() = SkyBlockTime.fromInstant(Instant.ofEpochMilli(millis))

fun elapsedMinutes() = passedSince().inWholeMinutes

companion object {

fun now() = SimpleTimeMark(System.currentTimeMillis())
Expand Down
Loading