Merge branch 'mc-1.21.3'

This commit is contained in:
Linnea Gräf
2024-12-31 16:52:29 +01:00
53 changed files with 2182 additions and 370 deletions

View File

@@ -0,0 +1,134 @@
package moe.nea.firmament.features.chat
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.StringReader
import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.tree.LiteralCommandNode
import kotlin.time.Duration.Companion.seconds
import net.minecraft.util.math.BlockPos
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.PartyMessageReceivedEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.useMatch
object PartyCommands {
val messageInChannel = "(?<channel>Party|Guild) >([^:]+?)? (?<name>[^: ]+): (?<message>.+)".toPattern()
@Subscribe
fun onChat(event: ProcessChatEvent) {
messageInChannel.useMatch(event.unformattedString) {
val channel = group("channel")
val message = group("message")
val name = group("name")
if (channel == "Party") {
PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent(
event, message, name
))
}
}
}
val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet()
data class PartyCommandContext(
val name: String
)
val dispatch = CommandDispatcher<PartyCommandContext>().also { dispatch ->
fun register(
name: String,
vararg alias: String,
block: CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>.() -> Unit = {},
): LiteralCommandNode<PartyCommandContext> {
val node =
dispatch.register(CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>(name).also(block))
alias.forEach { register(it) { redirect(node) } }
return node
}
register("warp", "pw", "pwarp", "partywarp") {
executes {
// TODO: add check if you are the party leader
MC.sendCommand("p warp")
0
}
}
register("transfer", "pt", "ptme") {
executes {
MC.sendCommand("p transfer ${it.source.name}")
0
}
}
register("allinvite", "allinv") {
executes {
MC.sendCommand("p settings allinvite")
0
}
}
register("coords") {
executes {
val p = MC.player?.blockPos ?: BlockPos.ORIGIN
MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}")
0
}
}
// TODO: downtime tracker (display message again at end of dungeon)
// instance ends: kuudra, dungeons, bacte
// TODO: at TPS command
}
object TConfig : ManagedConfig("party-commands", Category.CHAT) {
val enable by toggle("enable") { false }
val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds }
val ignoreOwnCommands by toggle("ignore-own") { false }
}
var lastCommand = TimeMark.farPast()
@Subscribe
fun listPartyCommands(event: CommandEvent.SubCommand) {
event.subcommand("partycommands") {
thenExecute {
// TODO: Better help, including descriptions and redirect detection
MC.sendChat(tr("firmament.partycommands.help", "Available party commands: ${dispatch.root.children.map { it.name }}. Available prefixes: $commandPrefixes"))
}
}
}
@Subscribe
fun onPartyMessage(event: PartyMessageReceivedEvent) {
if (!TConfig.enable) return
if (event.message.firstOrNull() !in commandPrefixes) return
if (event.name == MC.playerName && TConfig.ignoreOwnCommands) return
if (lastCommand.passedTime() < TConfig.cooldown) {
MC.sendChat(tr("firmament.partycommands.cooldown", "Skipping party command. Cooldown not passed."))
return
}
// TODO: add trust levels
val commandLine = event.message.substring(1)
try {
dispatch.execute(StringReader(commandLine), PartyCommandContext(event.name))
} catch (ex: Exception) {
if (ex is CommandSyntaxException) {
MC.sendChat(tr("firmament.partycommands.unknowncommand", "Unknown party command."))
return
} else {
MC.sendChat(tr("firmament.partycommands.unknownerror", "Unknown error during command execution."))
ErrorUtil.softError("Unknown error during command execution.", ex)
}
}
lastCommand = TimeMark.now()
}
}

View File

@@ -1,8 +1,12 @@
package moe.nea.firmament.features.chat
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DefaultSource
@@ -12,89 +16,139 @@ import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedOption
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.tr
object QuickCommands : FirmamentFeature {
override val identifier: String
get() = "quick-commands"
override val identifier: String
get() = "quick-commands"
fun removePartialPrefix(text: String, prefix: String): String? {
var lf: String? = null
for (i in 1..prefix.length) {
if (text.startsWith(prefix.substring(0, i))) {
lf = text.substring(i)
}
}
return lf
}
object TConfig : ManagedConfig("quick-commands", Category.CHAT) {
val enableJoin by toggle("join") { true }
val enableDh by toggle("dh") { true }
override fun onChange(option: ManagedOption<*>) {
reloadCommands()
}
}
val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
fun reloadCommands() {
val lastPacket = lastReceivedTreePacket ?: return
val network = MC.networkHandler ?: return
val fallback = ClientCommandInternals.getActiveDispatcher()
try {
val dispatcher = CommandDispatcher<FabricClientCommandSource>()
ClientCommandInternals.setActiveDispatcher(dispatcher)
ClientCommandRegistrationCallback.EVENT.invoker()
.register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries,
network.enabledFeatures))
ClientCommandInternals.finalizeInit()
network.onCommandTree(lastPacket)
} catch (ex: Exception) {
ClientCommandInternals.setActiveDispatcher(fallback)
throw ex
}
}
@Subscribe
fun onCommands(it: CommandEvent) {
it.register("join") {
thenArgument("what", RestArgumentType) { what ->
thenExecute {
val what = this[what]
if (!SBData.isOnSkyblock) {
MC.sendCommand("join $what")
return@thenExecute
}
val joinName = getNameForFloor(what.replace(" ", "").lowercase())
if (joinName == null) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
} else {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
joinName))
MC.sendCommand("joininstance $joinName")
}
}
}
thenExecute {
source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
}
}
}
fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
if (kuudraLevel != null) {
val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
it.startsWith(
kuudraLevel,
true
)
}
if (l !in kuudraLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
kuudraLevel))
return null
}
return "KUUDRA_${kuudraLevelNames[l]}"
}
val masterLevel = removePartialPrefix(w, "master")
val normalLevel =
removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
val dungeonLevel = masterLevel ?: normalLevel
if (dungeonLevel != null) {
val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
it.startsWith(
dungeonLevel,
true
)
}
if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
return "CATACOMBS_ENTRANCE"
}
if (l !in dungeonLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
kuudraLevel))
return null
}
return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
}
return null
}
fun removePartialPrefix(text: String, prefix: String): String? {
var lf: String? = null
for (i in 1..prefix.length) {
if (text.startsWith(prefix.substring(0, i))) {
lf = text.substring(i)
}
}
return lf
}
var lastReceivedTreePacket: CommandTreeS2CPacket? = null
val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
@Subscribe
fun registerDh(event: CommandEvent) {
if (!TConfig.enableDh) return
event.register("dh") {
thenExecute {
MC.sendCommand("warp dhub")
}
}
event.register("dn") {
thenExecute {
MC.sendChat(tr("firmament.quickwarp.deez-nutz", "Warping to... Deez Nuts!").grey())
MC.sendCommand("warp dhub")
}
}
}
@Subscribe
fun registerJoin(it: CommandEvent) {
if (!TConfig.enableJoin) return
it.register("join") {
thenArgument("what", RestArgumentType) { what ->
thenExecute {
val what = this[what]
if (!SBData.isOnSkyblock) {
MC.sendCommand("join $what")
return@thenExecute
}
val joinName = getNameForFloor(what.replace(" ", "").lowercase())
if (joinName == null) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
} else {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
joinName))
MC.sendCommand("joininstance $joinName")
}
}
}
thenExecute {
source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
}
}
}
fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
if (kuudraLevel != null) {
val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
it.startsWith(
kuudraLevel,
true
)
}
if (l !in kuudraLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
kuudraLevel))
return null
}
return "KUUDRA_${kuudraLevelNames[l]}"
}
val masterLevel = removePartialPrefix(w, "master")
val normalLevel =
removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
val dungeonLevel = masterLevel ?: normalLevel
if (dungeonLevel != null) {
val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
it.startsWith(
dungeonLevel,
true
)
}
if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
return "CATACOMBS_ENTRANCE"
}
if (l !in dungeonLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
kuudraLevel))
return null
}
return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
}
return null
}
}

View File

@@ -1,71 +1,67 @@
package moe.nea.firmament.features.fixes
import moe.nea.jarvis.api.Point
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.client.MinecraftClient
import net.minecraft.client.option.KeyBinding
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.minecraft.util.Arm
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.errorBoundary
object Fixes : FirmamentFeature {
override val identifier: String
get() = "fixes"
override val identifier: String
get() = "fixes"
object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
}
object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
val hidePotionEffects by toggle("hide-mob-effects") { false }
}
override val config: ManagedConfig
get() = TConfig
override val config: ManagedConfig
get() = TConfig
fun handleIsPressed(
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
cir.returnValue = true
}
fun handleIsPressed(
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
cir.returnValue = true
}
@Subscribe
fun onRenderHud(it: HudRenderEvent) {
if (!TConfig.autoSprintKeyBinding.isBound) return
it.context.matrices.push()
TConfig.autoSprintHud.applyTransformations(it.context.matrices)
it.context.drawText(
MC.font, Text.translatable(
if (TConfig.autoSprint)
"firmament.fixes.auto-sprint.on"
else if (MC.player?.isSprinting == true)
"firmament.fixes.auto-sprint.sprinting"
else
"firmament.fixes.auto-sprint.not-sprinting"
), 0, 0, -1, false
)
it.context.matrices.pop()
}
@Subscribe
fun onRenderHud(it: HudRenderEvent) {
if (!TConfig.autoSprintKeyBinding.isBound) return
it.context.matrices.push()
TConfig.autoSprintHud.applyTransformations(it.context.matrices)
it.context.drawText(
MC.font, Text.translatable(
if (TConfig.autoSprint)
"firmament.fixes.auto-sprint.on"
else if (MC.player?.isSprinting == true)
"firmament.fixes.auto-sprint.sprinting"
else
"firmament.fixes.auto-sprint.not-sprinting"
), 0, 0, -1, false
)
it.context.matrices.pop()
}
@Subscribe
fun onWorldKeyboard(it: WorldKeyboardEvent) {
if (it.matches(TConfig.autoSprintKeyBinding)) {
TConfig.autoSprint = !TConfig.autoSprint
}
}
@Subscribe
fun onWorldKeyboard(it: WorldKeyboardEvent) {
if (it.matches(TConfig.autoSprintKeyBinding)) {
TConfig.autoSprint = !TConfig.autoSprint
}
}
fun shouldPeekChat(): Boolean {
return TConfig.peekChat.isPressed(atLeast = true)
}
fun shouldPeekChat(): Boolean {
return TConfig.peekChat.isPressed(atLeast = true)
}
}

View File

@@ -29,18 +29,7 @@ object ItemRarityCosmetics : FirmamentFeature {
override val config: ManagedConfig
get() = TConfig
private val rarityToColor = mapOf(
Rarity.COMMON to Formatting.WHITE,
Rarity.UNCOMMON to Formatting.GREEN,
Rarity.RARE to Formatting.BLUE,
Rarity.EPIC to Formatting.DARK_PURPLE,
Rarity.LEGENDARY to Formatting.GOLD,
Rarity.MYTHIC to Formatting.LIGHT_PURPLE,
Rarity.DIVINE to Formatting.AQUA,
Rarity.SPECIAL to Formatting.RED,
Rarity.VERY_SPECIAL to Formatting.RED,
Rarity.SUPREME to Formatting.DARK_RED,
).mapValues {
private val rarityToColor = Rarity.colourMap.mapValues {
val c = Color(it.value.colorValue!!)
c.rgb
}

View File

@@ -2,7 +2,6 @@
package moe.nea.firmament.features.inventory
import com.mojang.blaze3d.systems.RenderSystem
import java.util.UUID
import org.lwjgl.glfw.GLFW
import kotlinx.serialization.Serializable
@@ -14,6 +13,7 @@ import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.util.Identifier
import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
@@ -59,6 +59,17 @@ object SlotLocking : FirmamentFeature {
}
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES }
}
enum class SlotRenderLinesMode : StringIdentifiable {
EVERYTHING,
ONLY_BOXES,
NOTHING;
override fun asString(): String {
return name
}
}
override val config: TConfig
@@ -95,7 +106,7 @@ object SlotLocking : FirmamentFeature {
if (handler.inventory.size() < 9) return false
val sellItem = handler.inventory.getStack(handler.inventory.size() - 5)
if (sellItem == null) return false
if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true
if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true
val lore = sellItem.loreAccordingToNbt
return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!"
}
@@ -104,7 +115,7 @@ object SlotLocking : FirmamentFeature {
fun onSalvageProtect(event: IsSlotProtectedEvent) {
if (event.slot == null) return
if (!event.slot.hasStack()) return
if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return
if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return
val inv = event.slot.inventory
var anyBlocked = false
for (i in 0 until event.slot.index) {
@@ -227,23 +238,32 @@ object SlotLocking : FirmamentFeature {
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
boundSlots.entries.forEach {
val hotbarSlot = findByIndex(it.key) ?: return@forEach
val inventorySlot = findByIndex(it.value) ?: return@forEach
for (it in boundSlots.entries) {
val hotbarSlot = findByIndex(it.key) ?: continue
val inventorySlot = findByIndex(it.value) ?: continue
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot
|| accScreen.focusedSlot_Firmament === inventorySlot
if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING)
continue
val color = if (anyHovered)
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
else
me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered)
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
color
)
event.context.drawBorder(hotbarSlot.x + sx,
hotbarSlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
16, 16, color.color)
event.context.drawBorder(inventorySlot.x + sx,
inventorySlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
16, 16, color.color)
}
}

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,3 @@
package moe.nea.firmament.features.inventory.storageoverlay
import io.ktor.util.decodeBase64Bytes
@@ -19,47 +17,59 @@ import net.minecraft.nbt.NbtIo
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtSizeTracker
import net.minecraft.registry.RegistryOps
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.mc.TolerantRegistriesOps
@Serializable(with = VirtualInventory.Serializer::class)
data class VirtualInventory(
val stacks: List<ItemStack>
val stacks: List<ItemStack>
) {
val rows = stacks.size / 9
val rows = stacks.size / 9
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
}
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
}
object Serializer : KSerializer<VirtualInventory> {
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
object Serializer : KSerializer<VirtualInventory> {
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
return VirtualInventory(items.map {
it as NbtCompound
if (it.isEmpty) ItemStack.EMPTY
else runCatching {
ItemStack.CODEC.parse(NbtOps.INSTANCE, it).orThrow
}.getOrElse { ItemStack.EMPTY }
})
}
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
val ops = getOps()
return VirtualInventory(items.map {
it as NbtCompound
if (it.isEmpty) ItemStack.EMPTY
else ErrorUtil.catch("Could not deserialize item") {
ItemStack.CODEC.parse(ops, it).orThrow
}.or { ItemStack.EMPTY }
})
}
override fun serialize(encoder: Encoder, value: VirtualInventory) {
val list = NbtList()
value.stacks.forEach {
if (it.isEmpty) list.add(NbtCompound())
else list.add(runCatching { ItemStack.CODEC.encode(it, NbtOps.INSTANCE, NbtCompound()).orThrow }
.getOrElse { NbtCompound() })
}
val baos = ByteArrayOutputStream()
NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
encoder.encodeString(baos.toByteArray().encodeBase64())
}
}
fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries)
override fun serialize(encoder: Encoder, value: VirtualInventory) {
val list = NbtList()
val ops = getOps()
value.stacks.forEach {
if (it.isEmpty) list.add(NbtCompound())
else list.add(ErrorUtil.catch("Could not serialize item") {
ItemStack.CODEC.encode(it,
ops,
NbtCompound()).orThrow
}
.or { NbtCompound() })
}
val baos = ByteArrayOutputStream()
NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
encoder.encodeString(baos.toByteArray().encodeBase64())
}
}
}

View File

@@ -50,7 +50,6 @@ object PickaxeAbility : FirmamentFeature {
val drillFuelBar by toggle("fuel-bar") { true }
val blockOnPrivateIsland by choice(
"block-on-dynamic",
BlockPickaxeAbility.entries,
) {
BlockPickaxeAbility.ONLY_DESTRUCTIVE
}
@@ -99,6 +98,7 @@ object PickaxeAbility : FirmamentFeature {
@Subscribe
fun onPickaxeRightClick(event: UseItemEvent) {
if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return
if (SBData.skyblockLocation != SkyBlockIsland.PRIVATE_ISLAND && SBData.skyblockLocation != SkyBlockIsland.GARDEN) return
val itemType = ItemType.fromItemStack(event.item)
if (itemType !in pickaxeTypes) return
val ability = AbilityUtils.getAbilities(event.item)

View File

@@ -0,0 +1,124 @@
package moe.nea.firmament.features.misc
import com.mojang.brigadier.arguments.IntegerArgumentType
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DurationArgumentType
import moe.nea.firmament.commands.RestArgumentType
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.util.CommonSoundEffects
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MinecraftDispatcher
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.clickCommand
import moe.nea.firmament.util.lime
import moe.nea.firmament.util.red
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.yellow
object TimerFeature {
data class Timer(
val start: TimeMark,
val duration: Duration,
val message: String,
val timerId: Int,
) {
fun timeLeft() = (duration - start.passedTime()).coerceAtLeast(0.seconds)
fun isDone() = start.passedTime() >= duration
}
// Theoretically for optimal performance this could be a treeset keyed to the end time
val timers = mutableListOf<Timer>()
@Subscribe
fun tick(event: TickEvent) {
timers.removeAll {
if (it.isDone()) {
MC.sendChat(tr("firmament.timer.finished",
"The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}")
.yellow())
Firmament.coroutineScope.launch {
withContext(MinecraftDispatcher) {
repeat(5) {
CommonSoundEffects.playSuccess()
delay(0.2.seconds)
}
}
}
true
} else {
false
}
}
}
fun startTimer(duration: Duration, message: String) {
val timerId = createTimerId++
timers.add(Timer(TimeMark.now(), duration, message, timerId))
MC.sendChat(
tr("firmament.timer.start",
"Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime()
.append(" ")
.append(
tr("firmament.timer.cancelbutton",
"Click here to cancel the timer."
).clickCommand("/firm timer clear $timerId").red()
)
)
}
fun clearTimer(timerId: Int) {
val timer = timers.indexOfFirst { it.timerId == timerId }
if (timer < 0) {
MC.sendChat(tr("firmament.timer.cancel.fail",
"Could not cancel that timer. Maybe it was already cancelled?").red())
} else {
val timerData = timers[timer]
timers.removeAt(timer)
MC.sendChat(tr("firmament.timer.cancel.done",
"Cancelled timer ${timerData.message}. It would have been done in ${
FirmFormatters.formatTimespan(timerData.timeLeft())
}.").lime())
}
}
var createTimerId = 0
@Subscribe
fun onCommands(event: CommandEvent.SubCommand) {
event.subcommand("cleartimer") {
thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId ->
thenExecute {
clearTimer(this[timerId])
}
}
thenExecute {
timers.map { it.timerId }.forEach {
clearTimer(it)
}
}
}
event.subcommand("timer") {
thenArgument("time", DurationArgumentType) { duration ->
thenExecute {
startTimer(this[duration], "no message")
}
thenArgument("message", RestArgumentType) { message ->
thenExecute {
startTimer(this[duration], this[message])
}
}
}
}
}
}