feat: Add lore timers

This commit is contained in:
Linnea Gräf
2024-12-24 01:58:46 +01:00
parent 24110c24af
commit fbab19b40f
3 changed files with 199 additions and 56 deletions

View 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())
)
}
}
}

View File

@@ -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))
}
}
}
}
} }

View File

@@ -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.",