aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt130
-rw-r--r--src/main/kotlin/util/SBData.kt116
2 files changed, 190 insertions, 56 deletions
diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt
new file mode 100644
index 0000000..f1b77c6
--- /dev/null
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -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())
+ )
+ }
+ }
+
+}
diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt
index 051d070..e785ff6 100644
--- a/src/main/kotlin/util/SBData.kt
+++ b/src/main/kotlin/util/SBData.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.util
+import java.time.ZoneId
import java.util.UUID
import net.hypixel.modapi.HypixelModAPI
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.ServerConnectedEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
-import moe.nea.firmament.events.WorldReadyEvent
object SBData {
- private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
- val profileSuggestTexts = listOf(
- "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
- "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
- )
- var profileId: UUID? = null
+ private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
+ val profileSuggestTexts = listOf(
+ "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
+ "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
+ )
+ var profileId: UUID? = null
- private var hasReceivedProfile = false
- var locraw: Locraw? = null
- val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
- val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
- val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
- var profileIdCommandDebounce = TimeMark.farPast()
- fun init() {
- ServerConnectedEvent.subscribe("SBData:onServerConnected") {
- HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
- }
- HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
- MC.onMainThread {
- val lastLocraw = locraw
- locraw = Locraw(it.serverName,
- it.serverType.getOrNull()?.name?.uppercase(),
- it.mode.getOrNull(),
- it.map.getOrNull())
- SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
- profileIdCommandDebounce = TimeMark.now()
- }
- }
- SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
- if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
- profileIdCommandDebounce = TimeMark.now()
- MC.sendServerCommand("profileid")
- }
- }
- AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
- if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
- event.cancel()
- }
- }
- ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
- val profileMatch = profileRegex.matchEntire(event.unformattedString)
- if (profileMatch != null) {
- val oldProfile = profileId
- try {
- profileId = UUID.fromString(profileMatch.groupValues[1])
- hasReceivedProfile = true
- } catch (e: IllegalArgumentException) {
- profileId = null
- e.printStackTrace()
- }
- if (oldProfile != profileId) {
- ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
- }
- }
- }
- }
+ /**
+ * Source: https://hypixel-skyblock.fandom.com/wiki/Time_Systems
+ */
+ val hypixelTimeZone = ZoneId.of("US/Eastern")
+ private var hasReceivedProfile = false
+ var locraw: Locraw? = null
+ val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
+ val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
+ val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
+ var profileIdCommandDebounce = TimeMark.farPast()
+ fun init() {
+ ServerConnectedEvent.subscribe("SBData:onServerConnected") {
+ HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
+ }
+ HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
+ MC.onMainThread {
+ val lastLocraw = locraw
+ locraw = Locraw(it.serverName,
+ it.serverType.getOrNull()?.name?.uppercase(),
+ it.mode.getOrNull(),
+ it.map.getOrNull())
+ SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
+ profileIdCommandDebounce = TimeMark.now()
+ }
+ }
+ SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
+ if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
+ profileIdCommandDebounce = TimeMark.now()
+ MC.sendServerCommand("profileid")
+ }
+ }
+ AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
+ if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
+ event.cancel()
+ }
+ }
+ ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
+ val profileMatch = profileRegex.matchEntire(event.unformattedString)
+ if (profileMatch != null) {
+ val oldProfile = profileId
+ try {
+ profileId = UUID.fromString(profileMatch.groupValues[1])
+ hasReceivedProfile = true
+ } catch (e: IllegalArgumentException) {
+ profileId = null
+ e.printStackTrace()
+ }
+ if (oldProfile != profileId) {
+ ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
+ }
+ }
+ }
+ }
}