feat: Add lore timers
This commit is contained in:
130
src/main/kotlin/features/inventory/TimerInLore.kt
Normal file
130
src/main/kotlin/features/inventory/TimerInLore.kt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package moe.nea.firmament.features.inventory
|
||||||
|
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.DateTimeFormatterBuilder
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
import java.time.temporal.ChronoField
|
||||||
|
import net.minecraft.text.Text
|
||||||
|
import net.minecraft.util.StringIdentifiable
|
||||||
|
import moe.nea.firmament.annotations.Subscribe
|
||||||
|
import moe.nea.firmament.events.ItemTooltipEvent
|
||||||
|
import moe.nea.firmament.gui.config.ManagedConfig
|
||||||
|
import moe.nea.firmament.util.SBData
|
||||||
|
import moe.nea.firmament.util.aqua
|
||||||
|
import moe.nea.firmament.util.grey
|
||||||
|
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
|
||||||
|
import moe.nea.firmament.util.tr
|
||||||
|
import moe.nea.firmament.util.unformattedString
|
||||||
|
|
||||||
|
object TimerInLore {
|
||||||
|
object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
|
||||||
|
val showTimers by toggle("show") { true }
|
||||||
|
val timerFormat by choice("format") { TimerFormat.SOCIALIST }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable {
|
||||||
|
RFC(DateTimeFormatter.RFC_1123_DATE_TIME),
|
||||||
|
LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)),
|
||||||
|
SOCIALIST(
|
||||||
|
{
|
||||||
|
appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT)
|
||||||
|
appendLiteral(" ")
|
||||||
|
appendValue(ChronoField.DAY_OF_MONTH, 2)
|
||||||
|
appendLiteral(".")
|
||||||
|
appendValue(ChronoField.MONTH_OF_YEAR, 2)
|
||||||
|
appendLiteral(".")
|
||||||
|
appendValue(ChronoField.YEAR, 4)
|
||||||
|
appendLiteral(" ")
|
||||||
|
appendValue(ChronoField.HOUR_OF_DAY, 2)
|
||||||
|
appendLiteral(":")
|
||||||
|
appendValue(ChronoField.MINUTE_OF_HOUR, 2)
|
||||||
|
appendLiteral(":")
|
||||||
|
appendValue(ChronoField.SECOND_OF_MINUTE, 2)
|
||||||
|
}),
|
||||||
|
AMERICAN("EEEE, MMM d h:mm a yyyy"),
|
||||||
|
;
|
||||||
|
|
||||||
|
constructor(block: DateTimeFormatterBuilder.() -> Unit)
|
||||||
|
: this(DateTimeFormatterBuilder().also(block).toFormatter())
|
||||||
|
|
||||||
|
constructor(format: String) : this(DateTimeFormatter.ofPattern(format))
|
||||||
|
|
||||||
|
override fun asString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CountdownTypes(
|
||||||
|
val match: String,
|
||||||
|
val label: String, // TODO: convert to a string
|
||||||
|
val isRelative: Boolean = false,
|
||||||
|
) {
|
||||||
|
STARTING("Starting in:", "Starts at"),
|
||||||
|
STARTS("Starts in:", "Starts at"),
|
||||||
|
INTEREST("Interest in:", "Interest at"),
|
||||||
|
UNTILINTEREST("Until interest:", "Interest at"),
|
||||||
|
ENDS("Ends in:", "Ends at"),
|
||||||
|
REMAINING("Remaining:", "Ends at"),
|
||||||
|
DURATION("Duration:", "Finishes at"),
|
||||||
|
TIMELEFT("Time left:", "Ends at"),
|
||||||
|
EVENTTIMELEFT("Event lasts for", "Ends at", isRelative = true),
|
||||||
|
SHENSUCKS("Auction ends in:", "Auction ends at"),
|
||||||
|
ENDS_PET_LEVELING(
|
||||||
|
"Ends:",
|
||||||
|
"Finishes at"
|
||||||
|
),
|
||||||
|
CALENDARDETAILS(" (§e", "Starts at"),
|
||||||
|
COMMUNITYPROJECTS("Contribute again", "Come back at"),
|
||||||
|
CHOCOLATEFACTORY("Next Charge", "Available at"),
|
||||||
|
STONKSAUCTION("Auction ends in", "Ends at"),
|
||||||
|
LIZSTONKREDEMPTION("Resets in:", "Resets at");
|
||||||
|
}
|
||||||
|
|
||||||
|
val regex =
|
||||||
|
"(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex()
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun modifyLore(event: ItemTooltipEvent) {
|
||||||
|
if (!TConfig.showTimers) return
|
||||||
|
var lastTimer: ZonedDateTime? = null
|
||||||
|
for (i in event.lines.indices) {
|
||||||
|
val line = event.lines[i].unformattedString
|
||||||
|
val countdownType = CountdownTypes.entries.find { it.match in line } ?: continue
|
||||||
|
if (countdownType == CountdownTypes.CALENDARDETAILS
|
||||||
|
&& !event.stack.displayNameAccordingToNbt.unformattedString.startsWith("Day ")
|
||||||
|
) continue
|
||||||
|
|
||||||
|
val countdownMatch = regex.findAll(line).filter { it.value.isNotBlank() }.lastOrNull() ?: continue
|
||||||
|
val (years, days, hours, minutes, seconds) =
|
||||||
|
listOf("years", "days", "hours", "minutes", "seconds")
|
||||||
|
.map {
|
||||||
|
countdownMatch.groups[it]?.value?.toLong() ?: 0L
|
||||||
|
}
|
||||||
|
if (years + days + hours + minutes + seconds == 0L) continue
|
||||||
|
var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone)
|
||||||
|
if (countdownType.isRelative) {
|
||||||
|
if (lastTimer == null) {
|
||||||
|
event.lines.add(i + 1,
|
||||||
|
tr("firmament.loretimer.missingrelative",
|
||||||
|
"Found a relative countdown with no baseline (Firmament)").grey())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
baseLine = lastTimer
|
||||||
|
}
|
||||||
|
val timer =
|
||||||
|
baseLine.plusYears(years).plusDays(days).plusHours(hours).plusMinutes(minutes).plusSeconds(seconds)
|
||||||
|
lastTimer = timer
|
||||||
|
val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault())
|
||||||
|
// TODO: install approximate time stabilization algorithm
|
||||||
|
event.lines.add(i + 1,
|
||||||
|
Text.literal("${countdownType.label}: ")
|
||||||
|
.grey()
|
||||||
|
.append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package moe.nea.firmament.util
|
package moe.nea.firmament.util
|
||||||
|
|
||||||
|
import java.time.ZoneId
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import net.hypixel.modapi.HypixelModAPI
|
import net.hypixel.modapi.HypixelModAPI
|
||||||
import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket
|
import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket
|
||||||
@@ -10,63 +11,66 @@ import moe.nea.firmament.events.ProcessChatEvent
|
|||||||
import moe.nea.firmament.events.ProfileSwitchEvent
|
import moe.nea.firmament.events.ProfileSwitchEvent
|
||||||
import moe.nea.firmament.events.ServerConnectedEvent
|
import moe.nea.firmament.events.ServerConnectedEvent
|
||||||
import moe.nea.firmament.events.SkyblockServerUpdateEvent
|
import moe.nea.firmament.events.SkyblockServerUpdateEvent
|
||||||
import moe.nea.firmament.events.WorldReadyEvent
|
|
||||||
|
|
||||||
object SBData {
|
object SBData {
|
||||||
private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
|
private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
|
||||||
val profileSuggestTexts = listOf(
|
val profileSuggestTexts = listOf(
|
||||||
"CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
|
"CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
|
||||||
"CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
|
"CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
|
||||||
)
|
)
|
||||||
var profileId: UUID? = null
|
var profileId: UUID? = null
|
||||||
|
|
||||||
private var hasReceivedProfile = false
|
/**
|
||||||
var locraw: Locraw? = null
|
* Source: https://hypixel-skyblock.fandom.com/wiki/Time_Systems
|
||||||
val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
|
*/
|
||||||
val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
|
val hypixelTimeZone = ZoneId.of("US/Eastern")
|
||||||
val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
|
private var hasReceivedProfile = false
|
||||||
var profileIdCommandDebounce = TimeMark.farPast()
|
var locraw: Locraw? = null
|
||||||
fun init() {
|
val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
|
||||||
ServerConnectedEvent.subscribe("SBData:onServerConnected") {
|
val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
|
||||||
HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
|
val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
|
||||||
}
|
var profileIdCommandDebounce = TimeMark.farPast()
|
||||||
HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
|
fun init() {
|
||||||
MC.onMainThread {
|
ServerConnectedEvent.subscribe("SBData:onServerConnected") {
|
||||||
val lastLocraw = locraw
|
HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
|
||||||
locraw = Locraw(it.serverName,
|
}
|
||||||
it.serverType.getOrNull()?.name?.uppercase(),
|
HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
|
||||||
it.mode.getOrNull(),
|
MC.onMainThread {
|
||||||
it.map.getOrNull())
|
val lastLocraw = locraw
|
||||||
SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
|
locraw = Locraw(it.serverName,
|
||||||
profileIdCommandDebounce = TimeMark.now()
|
it.serverType.getOrNull()?.name?.uppercase(),
|
||||||
}
|
it.mode.getOrNull(),
|
||||||
}
|
it.map.getOrNull())
|
||||||
SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
|
SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
|
||||||
if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
|
profileIdCommandDebounce = TimeMark.now()
|
||||||
profileIdCommandDebounce = TimeMark.now()
|
}
|
||||||
MC.sendServerCommand("profileid")
|
}
|
||||||
}
|
SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
|
||||||
}
|
if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
|
||||||
AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
|
profileIdCommandDebounce = TimeMark.now()
|
||||||
if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
|
MC.sendServerCommand("profileid")
|
||||||
event.cancel()
|
}
|
||||||
}
|
}
|
||||||
}
|
AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
|
||||||
ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
|
if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
|
||||||
val profileMatch = profileRegex.matchEntire(event.unformattedString)
|
event.cancel()
|
||||||
if (profileMatch != null) {
|
}
|
||||||
val oldProfile = profileId
|
}
|
||||||
try {
|
ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
|
||||||
profileId = UUID.fromString(profileMatch.groupValues[1])
|
val profileMatch = profileRegex.matchEntire(event.unformattedString)
|
||||||
hasReceivedProfile = true
|
if (profileMatch != null) {
|
||||||
} catch (e: IllegalArgumentException) {
|
val oldProfile = profileId
|
||||||
profileId = null
|
try {
|
||||||
e.printStackTrace()
|
profileId = UUID.fromString(profileMatch.groupValues[1])
|
||||||
}
|
hasReceivedProfile = true
|
||||||
if (oldProfile != profileId) {
|
} catch (e: IllegalArgumentException) {
|
||||||
ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
|
profileId = null
|
||||||
}
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
if (oldProfile != profileId) {
|
||||||
}
|
ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,15 @@
|
|||||||
"firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity",
|
"firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity",
|
||||||
"firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.",
|
"firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.",
|
||||||
"firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.",
|
"firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.",
|
||||||
|
"firmament.config.lore-timers": "Lore Timers",
|
||||||
|
"firmament.config.lore-timers.format": "Time Format",
|
||||||
|
"firmament.config.lore-timers.format.choice.american": "§9Ame§cri§fcan",
|
||||||
|
"firmament.config.lore-timers.format.choice.local": "System Time Format",
|
||||||
|
"firmament.config.lore-timers.format.choice.rfc": "RFC",
|
||||||
|
"firmament.config.lore-timers.format.choice.socialist": "European-ish",
|
||||||
|
"firmament.config.lore-timers.format.description": "Choose the time format in which resolved timers are displayed.",
|
||||||
|
"firmament.config.lore-timers.show": "Show Lore Timers",
|
||||||
|
"firmament.config.lore-timers.show.description": "Shows when a timer in a lore (such as interest, auction duration) would end.",
|
||||||
"firmament.config.party-commands": "Party Commands",
|
"firmament.config.party-commands": "Party Commands",
|
||||||
"firmament.config.party-commands.cooldown": "Cooldown",
|
"firmament.config.party-commands.cooldown": "Cooldown",
|
||||||
"firmament.config.party-commands.cooldown.description": "Prevent people from spamming commands with a delay between party commands.",
|
"firmament.config.party-commands.cooldown.description": "Prevent people from spamming commands with a delay between party commands.",
|
||||||
|
|||||||
Reference in New Issue
Block a user