From ba79b46f64d541504b391dae17efb485a08e7745 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 14:33:43 +0100 Subject: fix: Limit dangerous pickaxe abilities on garden as well --- src/main/kotlin/features/mining/PickaxeAbility.kt | 1 + 1 file changed, 1 insertion(+) (limited to 'src/main/kotlin/features') diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt index 2d6c3ee..a184c81 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -99,6 +99,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) -- cgit From a9a801048d694e5b250f3a23089075e9b7bd976c Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 15:32:18 +0100 Subject: feat: Option to not rendering slot binding lines --- src/main/kotlin/features/inventory/SlotLocking.kt | 44 ++++++++++++++++------- src/main/kotlin/gui/config/ManagedConfig.kt | 12 +++++++ translations/en_us.json | 5 +++ 3 files changed, 49 insertions(+), 12 deletions(-) (limited to 'src/main/kotlin/features') diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index fc09476..99130d5 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -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) } } diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt index 47a9c92..ea55f67 100644 --- a/src/main/kotlin/gui/config/ManagedConfig.kt +++ b/src/main/kotlin/gui/config/ManagedConfig.kt @@ -15,6 +15,7 @@ import org.lwjgl.glfw.GLFW import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlin.enums.enumEntries import kotlin.io.path.createDirectories import kotlin.io.path.readText import kotlin.io.path.writeText @@ -123,6 +124,17 @@ abstract class ManagedConfig( return option(propertyName, default, ChoiceHandler(universe)) } + protected inline fun choice( + propertyName: String, + noinline default: () -> E + ): ManagedOption where E : Enum, E : StringIdentifiable { + return choice(propertyName, enumEntries(), default) + } + + private fun createStringIdentifiable(x: () -> Array): Codec where E : Enum, E : StringIdentifiable { + return StringIdentifiable.createCodec { x() } + } + // TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434 // protected inline fun choice( // propertyName: String, diff --git a/translations/en_us.json b/translations/en_us.json index d191c84..668bfdc 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -202,6 +202,11 @@ "firmament.config.save-cursor-position.tolerance.description": "Select how long your cursor position last between GUIs before resetting back to the middle of the screen.", "firmament.config.slot-locking": "Slot Locking", "firmament.config.slot-locking.bind": "Bind Slot", + "firmament.config.slot-locking.bind-render": "Show Slot Bindings", + "firmament.config.slot-locking.bind-render.choice.everything": "Always", + "firmament.config.slot-locking.bind-render.choice.nothing": "Only when hovered", + "firmament.config.slot-locking.bind-render.choice.only_boxes": "Only boxes", + "firmament.config.slot-locking.bind-render.description": "Disable rendering of the slot binding lines (or all of the slot binding rendering), unless the relevant slot is being hovered.", "firmament.config.slot-locking.bind.description": "Bind a hotbar slot to another slot. This allows quick switching between the slots by shift clicking on either slot.", "firmament.config.slot-locking.lock": "Lock Slot", "firmament.config.slot-locking.lock-uuid": "Lock UUID (Lock Item)", -- cgit From 856f5e0e815c8ef3be77ef342e1b0ca91aab8b0a Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 16:08:31 +0100 Subject: fix: Add choice options to YACL integration --- gradle.properties | 3 +++ src/compat/yacl/java/YaclIntegration.kt | 29 +++++++++++++++++++++++ src/main/kotlin/features/mining/PickaxeAbility.kt | 1 - src/main/kotlin/gui/config/ChoiceHandler.kt | 1 + src/main/kotlin/gui/config/ManagedConfig.kt | 6 ++--- 5 files changed, 36 insertions(+), 4 deletions(-) (limited to 'src/main/kotlin/features') diff --git a/gradle.properties b/gradle.properties index ec779d5..2385103 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,3 +10,6 @@ archives_base_name=Firmament maven_group=moe.nea.firmament firmament.compiletimerepohash=a6116d945491d7c57c93d43f51250f93d62d8434 + +# TODO: remove after https://github.com/google/ksp/issues/2072 +ksp.incremental=false diff --git a/src/compat/yacl/java/YaclIntegration.kt b/src/compat/yacl/java/YaclIntegration.kt index 9aec501..45a0d02 100644 --- a/src/compat/yacl/java/YaclIntegration.kt +++ b/src/compat/yacl/java/YaclIntegration.kt @@ -11,9 +11,11 @@ import dev.isxander.yacl3.api.OptionGroup import dev.isxander.yacl3.api.YetAnotherConfigLib import dev.isxander.yacl3.api.controller.ControllerBuilder import dev.isxander.yacl3.api.controller.DoubleSliderControllerBuilder +import dev.isxander.yacl3.api.controller.EnumControllerBuilder import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder import dev.isxander.yacl3.api.controller.StringControllerBuilder import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder +import dev.isxander.yacl3.api.controller.ValueFormatter import dev.isxander.yacl3.gui.YACLScreen import dev.isxander.yacl3.gui.tab.ListHolderWidget import kotlin.time.Duration @@ -23,8 +25,10 @@ import net.minecraft.client.gui.Element import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text import moe.nea.firmament.gui.config.BooleanHandler +import moe.nea.firmament.gui.config.ChoiceHandler import moe.nea.firmament.gui.config.ClickHandler import moe.nea.firmament.gui.config.DurationHandler +import moe.nea.firmament.gui.config.EnumRenderer import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider import moe.nea.firmament.gui.config.HudMeta import moe.nea.firmament.gui.config.HudMetaHandler @@ -89,6 +93,10 @@ class YaclIntegration : FirmamentConfigScreenProvider { } .build() + is ChoiceHandler<*> -> return createDefaultBinding { + createChoiceBinding(handler as ChoiceHandler<*>, managedOption as ManagedOption<*>, it as Option<*>) + }.build() + is BooleanHandler -> return createDefaultBinding(TickBoxControllerBuilder::create).build() is StringHandler -> return createDefaultBinding(StringControllerBuilder::create).build() is IntegerHandler -> return createDefaultBinding { @@ -114,6 +122,27 @@ class YaclIntegration : FirmamentConfigScreenProvider { } } + private enum class Sacrifice {} + + private fun createChoiceBinding( + handler: ChoiceHandler<*>, + managedOption: ManagedOption<*>, + option: Option<*> + ): ControllerBuilder { + val b = EnumControllerBuilder.create(option as Option) + b.enumClass(handler.enumClass as Class) + /** + * This is a function with E to avoid realizing the Sacrifice outside of a `X` wrapper. + */ + fun > makeValueFormatter(): ValueFormatter { + return ValueFormatter { + (handler.renderer as EnumRenderer).getName(managedOption as ManagedOption, it) + } + } + b.formatValue(makeValueFormatter()) + return b as ControllerBuilder + } + fun buildConfig(): YetAnotherConfigLib { return YetAnotherConfigLib.createBuilder() diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt index a184c81..94b49f9 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -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 } diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt index 25e885a..2ea3efc 100644 --- a/src/main/kotlin/gui/config/ChoiceHandler.kt +++ b/src/main/kotlin/gui/config/ChoiceHandler.kt @@ -13,6 +13,7 @@ import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.json.KJsonOps class ChoiceHandler( + val enumClass: Class, val universe: List, ) : ManagedConfig.OptionHandler where E : Enum, E : StringIdentifiable { val codec = StringIdentifiable.createCodec { diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt index ea55f67..641b673 100644 --- a/src/main/kotlin/gui/config/ManagedConfig.kt +++ b/src/main/kotlin/gui/config/ManagedConfig.kt @@ -118,17 +118,17 @@ abstract class ManagedConfig( protected fun choice( propertyName: String, - universe: List, + enumClass: Class, default: () -> E ): ManagedOption where E : Enum, E : StringIdentifiable { - return option(propertyName, default, ChoiceHandler(universe)) + return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList())) } protected inline fun choice( propertyName: String, noinline default: () -> E ): ManagedOption where E : Enum, E : StringIdentifiable { - return choice(propertyName, enumEntries(), default) + return choice(propertyName, E::class.java, default) } private fun createStringIdentifiable(x: () -> Array): Codec where E : Enum, E : StringIdentifiable { -- cgit From 13994393ed392b33161cd427cc9730c9f1b11e35 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 16:29:18 +0100 Subject: feat: Hide potion effects --- .../firmament/mixins/HideStatusEffectsPatch.java | 29 +++++++ src/main/kotlin/features/fixes/Fixes.kt | 94 +++++++++++----------- translations/en_us.json | 2 + 3 files changed, 76 insertions(+), 49 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java (limited to 'src/main/kotlin/features') diff --git a/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java new file mode 100644 index 0000000..c5af8b6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java @@ -0,0 +1,29 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.ingame.StatusEffectsDisplay; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(InventoryScreen.class) +public abstract class HideStatusEffectsPatch { + @Shadow + public abstract boolean shouldHideStatusEffectHud(); + + @Inject(method = "shouldHideStatusEffectHud", at = @At("HEAD"), cancellable = true) + private void hideStatusEffects(CallbackInfoReturnable cir) { + cir.setReturnValue(!Fixes.TConfig.INSTANCE.getHidePotionEffects()); + } + + @WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/StatusEffectsDisplay;drawStatusEffects(Lnet/minecraft/client/gui/DrawContext;IIF)V")) + private boolean conditionalRenderStatuses(StatusEffectsDisplay instance, DrawContext context, int mouseX, int mouseY, float tickDelta) { + return shouldHideStatusEffectHud() || !Fixes.TConfig.INSTANCE.getHidePotionEffects(); + } + +} diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt index 5d70b1a..7030319 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -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 - ) { - if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true) - cir.returnValue = true - } + fun handleIsPressed( + keyBinding: KeyBinding, + cir: CallbackInfoReturnable + ) { + 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) + } } diff --git a/translations/en_us.json b/translations/en_us.json index 668bfdc..4d1801a 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -112,6 +112,8 @@ "firmament.config.fixes.auto-sprint-keybinding": "Auto Sprint KeyBinding", "firmament.config.fixes.auto-sprint-keybinding.description": "Toggle auto sprint via this keybinding.", "firmament.config.fixes.auto-sprint.description": "This is different from vanilla sprint in the way that it only marks the keybinding pressed for the first tick of walking.", + "firmament.config.fixes.hide-mob-effects": "Hide Potion Effects", + "firmament.config.fixes.hide-mob-effects.description": "Hide Potion effects on the right side of your player inventory.", "firmament.config.fixes.peek-chat": "Peek Chat", "firmament.config.fixes.peek-chat.description": "Hold this keybinding to view the chat as if you have it opened, but while still being able to control your character.", "firmament.config.fixes.player-skins": "Fix unsigned Player Skins", -- cgit From ebcb06df4092500a38e9a1a8d56d249b5ff37c47 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 21:17:11 +0100 Subject: feat: Add party commands --- .../kotlin/events/PartyMessageReceivedEvent.kt | 9 ++ src/main/kotlin/features/chat/PartyCommands.kt | 134 +++++++++++++++++++++ src/main/kotlin/util/MC.kt | 5 +- translations/en_us.json | 7 ++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/events/PartyMessageReceivedEvent.kt create mode 100644 src/main/kotlin/features/chat/PartyCommands.kt (limited to 'src/main/kotlin/features') diff --git a/src/main/kotlin/events/PartyMessageReceivedEvent.kt b/src/main/kotlin/events/PartyMessageReceivedEvent.kt new file mode 100644 index 0000000..4688dfe --- /dev/null +++ b/src/main/kotlin/events/PartyMessageReceivedEvent.kt @@ -0,0 +1,9 @@ +package moe.nea.firmament.events + +data class PartyMessageReceivedEvent( + val from: ProcessChatEvent, + val message: String, + val name: String, +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt new file mode 100644 index 0000000..de3a0d9 --- /dev/null +++ b/src/main/kotlin/features/chat/PartyCommands.kt @@ -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 = "(?Party|Guild) >([^:]+?)? (?[^: ]+): (?.+)".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().also { dispatch -> + fun register( + name: String, + vararg alias: String, + block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit = {}, + ): LiteralCommandNode { + val node = + dispatch.register(CaseInsensitiveLiteralCommandNode.Builder(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() + } +} diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index 294334a..215d2a8 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -64,6 +64,8 @@ object MC { } fun sendCommand(command: String) { + // TODO: add a queue to this and sendServerChat + ErrorUtil.softCheck("Server commands have an implied /", !command.startsWith("/")) player?.networkHandler?.sendCommand(command) } @@ -96,8 +98,9 @@ object MC { inline val camera: Entity? get() = instance.cameraEntity inline val guiAtlasManager get() = instance.guiAtlasManager inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } + inline val playerName: String? get() = player?.name?.unformattedString inline var screen: Screen? - get() = TestUtil.unlessTesting{ instance.currentScreen } + get() = TestUtil.unlessTesting { instance.currentScreen } set(value) = instance.setScreen(value) val screenName get() = screen?.title?.unformattedString?.trim() inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*> diff --git a/translations/en_us.json b/translations/en_us.json index 4d1801a..d1af11e 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -129,6 +129,13 @@ "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.description": "Show a background behind each item, depending on its rarity.", + "firmament.config.party-commands": "Party Commands", + "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.enable": "Enable Party Commands", + "firmament.config.party-commands.enable.description": "Allow people in your party to use commands like !warp, !coords, !ptme and so on. See /firm partycommands for a list", + "firmament.config.party-commands.ignore-own": "Ignore Own Messages", + "firmament.config.party-commands.ignore-own.description": "Prevent your own messages from triggering party commands", "firmament.config.pets": "Pets", "firmament.config.pets.highlight-pet": "Highlight active pet", "firmament.config.pets.highlight-pet.description": "Highlight your currently selected pet in the /pets menu.", -- cgit From 656958937fe29da90d7229b979996a68b5ea5b67 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Mon, 23 Dec 2024 23:02:08 +0100 Subject: feat: Add /dh command --- .../mixins/FirmKeybindsInVanillaControlsPatch.java | 2 +- .../mixins/SaveOriginalCommandTreePacket.java | 17 ++ src/main/kotlin/features/chat/QuickCommands.kt | 212 +++++++++++++-------- src/main/kotlin/gui/config/ManagedConfig.kt | 5 +- src/main/kotlin/gui/config/ManagedOption.kt | 9 +- src/main/resources/firmament.accesswidener | 1 + translations/en_us.json | 5 + 7 files changed, 167 insertions(+), 84 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java (limited to 'src/main/kotlin/features') diff --git a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java index b386604..699d5b7 100644 --- a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java @@ -51,7 +51,7 @@ public class FirmKeybindsInVanillaControlsPatch { var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding); if (config == null) return; resetButton.active = false; - editButton.setMessage(Text.translatable("firmament.keybinding.external", config.value.format())); + editButton.setMessage(Text.translatable("firmament.keybinding.external", config.getValue().format())); ci.cancel(); } diff --git a/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java new file mode 100644 index 0000000..2f2f188 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.chat.QuickCommands; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +public class SaveOriginalCommandTreePacket { + @Inject(method = "onCommandTree", at = @At(value = "RETURN")) + private void saveUnmodifiedCommandTree(CommandTreeS2CPacket packet, CallbackInfo ci) { + QuickCommands.INSTANCE.setLastReceivedTreePacket(packet); + } +} diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt index 5944b92..7963171 100644 --- a/src/main/kotlin/features/chat/QuickCommands.kt +++ b/src/main/kotlin/features/chat/QuickCommands.kt @@ -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" + + 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() + } + } + + fun reloadCommands() { + val lastPacket = lastReceivedTreePacket ?: return + val network = MC.networkHandler ?: return + val fallback = ClientCommandInternals.getActiveDispatcher() + try { + val dispatcher = CommandDispatcher() + 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 + } + } + + + 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 - 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 - } + val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") + val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") - 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 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")) - } - } - } + @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.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 CommandContext.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 + } } diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt index 641b673..7ddda9e 100644 --- a/src/main/kotlin/gui/config/ManagedConfig.kt +++ b/src/main/kotlin/gui/config/ManagedConfig.kt @@ -15,7 +15,6 @@ import org.lwjgl.glfw.GLFW import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject -import kotlin.enums.enumEntries import kotlin.io.path.createDirectories import kotlin.io.path.readText import kotlin.io.path.writeText @@ -135,7 +134,7 @@ abstract class ManagedConfig( return StringIdentifiable.createCodec { x() } } -// TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434 + // TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434 // protected inline fun choice( // propertyName: String, // noinline default: () -> E @@ -148,6 +147,8 @@ abstract class ManagedConfig( // default // ) // } + open fun onChange(option: ManagedOption<*>) { + } protected fun duration( propertyName: String, diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt index d1aba83..383f392 100644 --- a/src/main/kotlin/gui/config/ManagedOption.kt +++ b/src/main/kotlin/gui/config/ManagedOption.kt @@ -6,7 +6,6 @@ import kotlinx.serialization.json.JsonObject import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import net.minecraft.text.Text -import moe.nea.firmament.Firmament import moe.nea.firmament.util.ErrorUtil class ManagedOption( @@ -28,7 +27,13 @@ class ManagedOption( val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description" val labelDescription: Text = Text.translatable(descriptionTranslationKey) - lateinit var value: T + private var actualValue: T? = null + var value: T + get() = actualValue ?: error("Lateinit variable not initialized") + set(value) { + actualValue = value + element.onChange(this) + } override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = value diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index 8e7dbab..cfa8e90 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -3,6 +3,7 @@ accessible class net/minecraft/client/render/RenderLayer$MultiPhase accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters accessible class net/minecraft/client/font/TextRenderer$Drawer accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator; +accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable; accessible field net/minecraft/client/render/item/HeldItemRenderer itemRenderer Lnet/minecraft/client/render/item/ItemRenderer; accessible field net/minecraft/client/render/item/ItemModels missingModelSupplier Ljava/util/function/Supplier; diff --git a/translations/en_us.json b/translations/en_us.json index d1af11e..a157432 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -180,6 +180,11 @@ "firmament.config.pristine-profit.position.description": "Edit the pristine profit hud location.", "firmament.config.pristine-profit.timeout": "Timeout (0 = disabled)", "firmament.config.pristine-profit.timeout.description": "Track the profit you make from pristine gemstones while mining. Set to 0 seconds to disable the HUD.", + "firmament.config.quick-commands": "Quick Commands", + "firmament.config.quick-commands.dh": "Enable /dh", + "firmament.config.quick-commands.dh.description": "Warps you to the dungeon hub.", + "firmament.config.quick-commands.join": "Enable /join", + "firmament.config.quick-commands.join.description": "Join various types of instances like dungeons using short hands like /join f1, /join k1, /join m7", "firmament.config.repo": "Firmament Repo Settings", "firmament.config.repo.autoUpdate": "Auto Update", "firmament.config.repo.autoUpdate.description": "Automatically download new items for the item list on every startup.", -- cgit From 39d35afb702cf017569ef9594774561848db7494 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Mon, 23 Dec 2024 23:53:27 +0100 Subject: fix: Some items not being saved in /firm stoarge --- .../TolerateFirmamentTolerateRegistryOwners.java | 18 +++++ .../inventory/storageoverlay/VirtualInventory.kt | 82 ++++++++++++---------- src/main/kotlin/util/mc/TolerantRegistriesOps.kt | 29 ++++++++ src/main/resources/firmament.accesswidener | 2 + 4 files changed, 95 insertions(+), 36 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java create mode 100644 src/main/kotlin/util/mc/TolerantRegistriesOps.kt (limited to 'src/main/kotlin/features') diff --git a/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java new file mode 100644 index 0000000..ac6f614 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.util.mc.TolerantRegistriesOps; +import net.minecraft.registry.entry.RegistryEntryOwner; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RegistryEntryOwner.class) +public interface TolerateFirmamentTolerateRegistryOwners { + @Inject(method = "ownerEquals", at = @At("HEAD"), cancellable = true) + private void equalTolerantRegistryOwners(RegistryEntryOwner other, CallbackInfoReturnable cir) { + if (other instanceof TolerantRegistriesOps.TolerantOwner) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index e07df8a..3b86184 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -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 + val stacks: List ) { - 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 { + const val INVENTORY = "INVENTORY" + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING) - object Serializer : KSerializer { - 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()) + 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 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 } - }) - } + fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) - 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()) - } - } + 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()) + } + } } diff --git a/src/main/kotlin/util/mc/TolerantRegistriesOps.kt b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt new file mode 100644 index 0000000..ce596a0 --- /dev/null +++ b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt @@ -0,0 +1,29 @@ +package moe.nea.firmament.util.mc + +import com.mojang.serialization.DynamicOps +import java.util.Optional +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryOps +import net.minecraft.registry.RegistryWrapper +import net.minecraft.registry.entry.RegistryEntryOwner + +class TolerantRegistriesOps( + delegate: DynamicOps, + registryInfoGetter: RegistryInfoGetter +) : RegistryOps(delegate, registryInfoGetter) { + constructor(delegate: DynamicOps, registry: RegistryWrapper.WrapperLookup) : + this(delegate, CachedRegistryInfoGetter(registry)) + + class TolerantOwner : RegistryEntryOwner { + override fun ownerEquals(other: RegistryEntryOwner?): Boolean { + return true + } + } + + override fun getOwner(registryRef: RegistryKey>?): Optional> { + return super.getOwner(registryRef).map { + TolerantOwner() + } + } +} diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index cfa8e90..b280087 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -4,6 +4,8 @@ accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters accessible class net/minecraft/client/font/TextRenderer$Drawer accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator; accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable; +accessible method net/minecraft/registry/RegistryOps (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V +accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter accessible field net/minecraft/client/render/item/HeldItemRenderer itemRenderer Lnet/minecraft/client/render/item/ItemRenderer; accessible field net/minecraft/client/render/item/ItemModels missingModelSupplier Ljava/util/function/Supplier; -- cgit From 24110c24af3ed14c0da050a2cdb1053b183b30b6 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 24 Dec 2024 01:01:10 +0100 Subject: feat: Add /firm timer command --- .../AlwaysDisplayFirmamentClientCommandErrors.java | 18 +++ src/main/kotlin/commands/Duration.kt | 75 +++++++++++++ src/main/kotlin/features/misc/TimerFeature.kt | 124 +++++++++++++++++++++ src/main/kotlin/util/textutil.kt | 3 +- 4 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java create mode 100644 src/main/kotlin/commands/Duration.kt create mode 100644 src/main/kotlin/features/misc/TimerFeature.kt (limited to 'src/main/kotlin/features') diff --git a/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java new file mode 100644 index 0000000..59769c6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import net.fabricmc.fabric.impl.command.client.ClientCommandInternals; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ClientCommandInternals.class) +public class AlwaysDisplayFirmamentClientCommandErrors { + @ModifyExpressionValue(method = "executeCommand", at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/command/client/ClientCommandInternals;isIgnoredException(Lcom/mojang/brigadier/exceptions/CommandExceptionType;)Z")) + private static boolean markFirmamentExceptionsAsNotIgnores(boolean original, @Local(argsOnly = true) String command) { + if (command.startsWith("firm ") || command.equals("firm") || command.startsWith("firmament ") || command.equals("firmament")) { + return false; + } + return original; + } +} diff --git a/src/main/kotlin/commands/Duration.kt b/src/main/kotlin/commands/Duration.kt new file mode 100644 index 0000000..42f143d --- /dev/null +++ b/src/main/kotlin/commands/Duration.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.commands + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import java.util.concurrent.CompletableFuture +import java.util.function.Function +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import moe.nea.firmament.util.tr + +object DurationArgumentType : ArgumentType { + val unknownTimeCode = DynamicCommandExceptionType { timeCode -> + tr("firmament.command-argument.duration.error", + "Unknown time code '$timeCode'") + } + + override fun parse(reader: StringReader): Duration { + val start = reader.cursor + val string = reader.readUnquotedString() + val matcher = regex.matcher(string) + var s = 0 + var time = 0.seconds + fun createError(till: Int) { + throw unknownTimeCode.createWithContext( + reader.also { it.cursor = start + s }, + string.substring(s, till)) + } + + while (matcher.find()) { + if (matcher.start() != s) { + createError(matcher.start()) + } + s = matcher.end() + val amount = matcher.group("count").toDouble() + val what = timeSuffixes[matcher.group("what").single()]!! + time += amount.toDuration(what) + } + if (string.length != s) { + createError(string.length) + } + return time + } + + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder + ): CompletableFuture { + val remaining = builder.remainingLowerCase.substringBefore(' ') + if (remaining.isEmpty()) return super.listSuggestions(context, builder) + if (remaining.last().isDigit()) { + for (timeSuffix in timeSuffixes.keys) { + builder.suggest(remaining + timeSuffix) + } + } + return builder.buildFuture() + } + + val timeSuffixes = mapOf( + 'm' to DurationUnit.MINUTES, + 's' to DurationUnit.SECONDS, + 'h' to DurationUnit.HOURS, + ) + val regex = "(?[0-9]+)(?[${timeSuffixes.keys.joinToString("")}])".toPattern() + + override fun getExamples(): Collection { + return listOf("3m", "20s", "1h45m") + } +} diff --git a/src/main/kotlin/features/misc/TimerFeature.kt b/src/main/kotlin/features/misc/TimerFeature.kt new file mode 100644 index 0000000..7c4833d --- /dev/null +++ b/src/main/kotlin/features/misc/TimerFeature.kt @@ -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() + + @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]) + } + } + } + } + } +} diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 5d95d7a..06ed8c8 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -142,8 +142,7 @@ fun MutableText.bold(): MutableText = styled { it.withBold(true) } fun MutableText.clickCommand(command: String): MutableText { require(command.startsWith("/")) return this.styled { - it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, - "/firm disablereiwarning")) + it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command)) } } -- cgit From fbab19b40f72574b7930ddd2981998b2d2845471 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 24 Dec 2024 01:58:46 +0100 Subject: feat: Add lore timers --- src/main/kotlin/features/inventory/TimerInLore.kt | 130 ++++++++++++++++++++++ src/main/kotlin/util/SBData.kt | 116 +++++++++---------- translations/en_us.json | 9 ++ 3 files changed, 199 insertions(+), 56 deletions(-) create mode 100644 src/main/kotlin/features/inventory/TimerInLore.kt (limited to 'src/main/kotlin/features') 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)(?:(?[0-9]+) ?(y|years?) )?(?:(?[0-9]+) ?(d|days?))? ?(?:(?[0-9]+) ?(h|hours?))? ?(?:(?[0-9]+) ?(m|minutes?))? ?(?:(?[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)) + } + } + } + } } diff --git a/translations/en_us.json b/translations/en_us.json index a157432..3d8de96 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -129,6 +129,15 @@ "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.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.cooldown": "Cooldown", "firmament.config.party-commands.cooldown.description": "Prevent people from spamming commands with a delay between party commands.", -- cgit From ddebaf47900dfab41590c97c202984142ae5b9f6 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Wed, 25 Dec 2024 16:15:20 +0100 Subject: WIP: Reforge Recipes --- .../moe/nea/firmament/compat/rei/EntityWidget.kt | 34 ++++-- .../compat/rei/recipes/SBReforgeRecipe.kt | 52 ++++++-- .../features/inventory/ItemRarityCosmetics.kt | 13 +- src/main/kotlin/gui/entity/EntityRenderer.kt | 13 +- src/main/kotlin/repo/ItemCache.kt | 2 + src/main/kotlin/repo/Reforge.kt | 14 ++- src/main/kotlin/repo/SBItemStack.kt | 136 ++++++++++++++++++++- src/main/kotlin/util/AprilFoolsUtil.kt | 10 ++ src/main/kotlin/util/FirmFormatters.kt | 14 ++- src/main/kotlin/util/SkyblockId.kt | 5 +- src/main/kotlin/util/mc/NbtItemData.kt | 4 +- src/main/kotlin/util/skyblock/Rarity.kt | 17 ++- src/main/kotlin/util/skyblock/SkyBlockItems.kt | 1 + src/main/kotlin/util/textutil.kt | 6 + 14 files changed, 273 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/util/AprilFoolsUtil.kt (limited to 'src/main/kotlin/features') diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt index d8238be..2b9e4bf 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt @@ -1,19 +1,22 @@ package moe.nea.firmament.compat.rei import me.shedaniel.math.Dimension +import me.shedaniel.math.FloatingDimension import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.Drawable import net.minecraft.client.gui.Element -import net.minecraft.client.gui.ParentElement import net.minecraft.entity.LivingEntity import moe.nea.firmament.gui.entity.EntityRenderer import moe.nea.firmament.util.ErrorUtil -class EntityWidget(val entity: LivingEntity?, val point: Point) : WidgetWithBounds() { +class EntityWidget( + val entity: LivingEntity?, + val point: Point, + val size: FloatingDimension = FloatingDimension(defaultSize) +) : WidgetWithBounds() { override fun children(): List { return emptyList() } @@ -22,18 +25,35 @@ class EntityWidget(val entity: LivingEntity?, val point: Point) : WidgetWithBoun override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { try { - if (!hasErrored) - EntityRenderer.renderEntity(entity!!, context, point.x, point.y, mouseX.toFloat(), mouseY.toFloat()) + context.matrices.push() + if (!hasErrored) { + context.matrices.translate(point.x.toDouble(), point.y.toDouble(), 0.0) + val xScale = size.width / defaultSize.width.toDouble() + val yScale = size.height / defaultSize.height.toDouble() + context.matrices.scale(xScale.toFloat(), yScale.toFloat(), 1.0F) + EntityRenderer.renderEntity( + entity!!, + context, + 0, 0, + (mouseX - point.x) * xScale, + (mouseY - point.y) * yScale) + } } catch (ex: Exception) { ErrorUtil.softError("Failed to render constructed entity: $entity", ex) hasErrored = true + } finally { + context.matrices.pop() } if (hasErrored) { - context.fill(point.x, point.y, point.x + 50, point.y + 80, 0xFFAA2222.toInt()) + context.fill(point.x, point.y, point.x + size.width.toInt(), point.y + size.height.toInt(), 0xFFAA2222.toInt()) } } + companion object { + val defaultSize = Dimension(50, 80) + } + override fun getBounds(): Rectangle { - return Rectangle(point, Dimension(50, 80)) + return Rectangle(point, size) } } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt index 2805132..232f04f 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt @@ -1,6 +1,8 @@ package moe.nea.firmament.compat.rei.recipes import java.util.Optional +import me.shedaniel.math.Dimension +import me.shedaniel.math.FloatingDimension import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import me.shedaniel.rei.api.client.gui.Renderer @@ -14,17 +16,27 @@ import me.shedaniel.rei.api.common.display.Display import me.shedaniel.rei.api.common.display.DisplaySerializer import me.shedaniel.rei.api.common.entry.EntryIngredient import me.shedaniel.rei.api.common.entry.EntryStack +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnReason import net.minecraft.text.Text import net.minecraft.util.Identifier +import net.minecraft.village.VillagerProfession import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.EntityWidget import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.gui.entity.EntityRenderer import moe.nea.firmament.repo.Reforge import moe.nea.firmament.repo.ReforgeStore import moe.nea.firmament.repo.RepoItemTypeCache import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.AprilFoolsUtil +import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.gold import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.skyblock.SkyBlockItems import moe.nea.firmament.util.skyblockId import moe.nea.firmament.util.tr @@ -46,21 +58,44 @@ class SBReforgeRecipe( } override fun getIcon(): Renderer { - return SBItemEntryDefinition.getEntry(SkyblockId("REFORGE_ANVIL")) + return SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL) } override fun setupDisplay(display: SBReforgeRecipe, bounds: Rectangle): MutableList { val list = mutableListOf() list.add(Widgets.createRecipeBase(bounds)) // TODO: actual layout after christmas, probably - list.add(Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY)) + list.add(Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY - 9)) .markInput().entries(display.inputItems)) - val stoneSlot = Widgets.createSlot(Point(bounds.minX + 38, bounds.centerY)) - .markInput() - if (display.reforgeStone != null) - stoneSlot.entry(display.reforgeStone) - list.add(stoneSlot) - list.add(Widgets.createSlot(Point(bounds.minX + 38 + 18, bounds.centerY)) + if (display.reforgeStone != null) { + list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24, bounds.centerY - 9 - 10)) + .markInput().entry(display.reforgeStone)) + list.add(Widgets.withTooltip( + Widgets.withTranslate(Widgets.wrapRenderer( + Rectangle(Point(bounds.minX + 10 + 24, bounds.centerY - 9 + 10), Dimension(18, 18)), + SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL)), 0.0, 0.0, 150.0), + Rarity.entries.mapNotNull { rarity -> + display.reforge.reforgeCosts?.get(rarity)?.let { rarity to it } + }.map { (rarity, cost) -> + Text.literal("") + .append(rarity.text) + .append(": ") + .append(Text.literal("${FirmFormatters.formatCommas(cost, 0)} Coins").gold()) + } + )) + } else { + val size = if (AprilFoolsUtil.isAprilFoolsDay) 1.2 else 0.6 + val dimension = + FloatingDimension(EntityWidget.defaultSize.width * size, EntityWidget.defaultSize.height * size) + list.add(EntityWidget( + EntityType.VILLAGER.create(EntityRenderer.fakeWorld, SpawnReason.COMMAND) + ?.also { it.villagerData = it.villagerData.withProfession(VillagerProfession.WEAPONSMITH) }, + Point(bounds.minX + 10 + 24 + 8 - dimension.width / 2, bounds.centerY - dimension.height / 2), + dimension + )) +// TODO: render a blacksmith entity or smth + } + list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24 + 24, bounds.centerY - 9)) .markInput().entries(display.outputItems)) return list } @@ -106,6 +141,7 @@ class SBReforgeRecipe( when (it) { is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> listOfNotNull(RepoManager.getNEUItem(it.internalName)) + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> ReforgeStore.resolveItemType(it.itemType) .flatMap { diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt index d2c555b..fdc378a 100644 --- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -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 } diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt index 022b9a3..9a09fc6 100644 --- a/src/main/kotlin/gui/entity/EntityRenderer.kt +++ b/src/main/kotlin/gui/entity/EntityRenderer.kt @@ -111,8 +111,9 @@ object EntityRenderer { renderContext: DrawContext, posX: Int, posY: Int, - mouseX: Float, - mouseY: Float + // TODO: Add width, height properties here + mouseX: Double, + mouseY: Double ) { var bottomOffset = 0.0F var currentEntity = entity @@ -148,15 +149,15 @@ object EntityRenderer { y2: Int, size: Float, bottomOffset: Float, - mouseX: Float, - mouseY: Float, + mouseX: Double, + mouseY: Double, entity: LivingEntity ) { context.enableScissorWithTranslation(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat()) val centerX = (x1 + x2) / 2f val centerY = (y1 + y2) / 2f - val targetYaw = atan(((centerX - mouseX) / 40.0f).toDouble()).toFloat() - val targetPitch = atan(((centerY - mouseY) / 40.0f).toDouble()).toFloat() + val targetYaw = atan(((centerX - mouseX) / 40.0f)).toFloat() + val targetPitch = atan(((centerY - mouseY) / 40.0f)).toFloat() val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat()) val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180)) rotateToFaceTheFront.mul(rotateToFaceTheCamera) diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index f88dd48..9f1d45c 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -39,6 +39,7 @@ import moe.nea.firmament.util.TestUtil import moe.nea.firmament.util.directLiteralStringContent import moe.nea.firmament.util.mc.FirmamentDataComponentTypes import moe.nea.firmament.util.mc.appendLore +import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.mc.modifyLore import moe.nea.firmament.util.mc.setCustomName @@ -131,6 +132,7 @@ object ItemCache : IReloadable { val itemInstance = ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this) itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) } + itemInstance.displayNameAccordingToNbt = un189Lore(displayName) val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes") if (extraAttributes != null) itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)) diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt index ea01818..b52adc6 100644 --- a/src/main/kotlin/repo/Reforge.kt +++ b/src/main/kotlin/repo/Reforge.kt @@ -108,6 +108,8 @@ data class Reforge( @Serializable(with = RarityMapped.Serializer::class) sealed interface RarityMapped { + fun get(rarity: Rarity): T? + class Serializer( val values: KSerializer ) : KSerializer> { @@ -137,10 +139,18 @@ data class Reforge( } @Serializable - data class Direct(val value: T) : RarityMapped + data class Direct(val value: T) : RarityMapped { + override fun get(rarity: Rarity): T { + return value + } + } @Serializable - data class PerRarity(val values: Map) : RarityMapped + data class PerRarity(val values: Map) : RarityMapped { + override fun get(rarity: Rarity): T? { + return values[rarity] + } + } } } diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index e5cacaa..a5f54ae 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -9,22 +9,31 @@ import net.minecraft.item.ItemStack import net.minecraft.network.RegistryByteBuf import net.minecraft.network.codec.PacketCodec import net.minecraft.network.codec.PacketCodecs +import net.minecraft.text.Style import net.minecraft.text.Text +import net.minecraft.text.TextColor import net.minecraft.util.Formatting import moe.nea.firmament.repo.ItemCache.asItemStack import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.LegacyFormattingCode import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.blue +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.getReforgeId import moe.nea.firmament.util.getUpgradeStars +import moe.nea.firmament.util.grey import moe.nea.firmament.util.mc.appendLore import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.petData +import moe.nea.firmament.util.prepend import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblock.Rarity import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.useMatch import moe.nea.firmament.util.withColor data class SBItemStack constructor( @@ -84,6 +93,117 @@ data class SBItemStack constructor( } return SBItemStack(neuIngredient.skyblockId, neuIngredient.amount.toInt()) } + + fun appendEnhancedStats( + itemStack: ItemStack, + reforgeStats: Map, + buffKind: BuffKind, + ) { + val namedReforgeStats = reforgeStats + .mapKeysTo(mutableMapOf()) { statIdToName(it.key) } + val loreMut = itemStack.loreAccordingToNbt.toMutableList() + var statBlockLastIndex = -1 + for (i in loreMut.indices) { + val statLine = parseStatLine(loreMut[i]) + if (statLine == null && statBlockLastIndex >= 0) { + break + } + if (statLine == null) { + continue + } + statBlockLastIndex = i + val statBuff = namedReforgeStats.remove(statLine.statName) ?: continue + loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute() + } + if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) { + loreMut.add(0, Text.literal("")) + } + // If there is no stat block the statBlockLastIndex falls through to -1 + // TODO: this is good enough for some items. some other items might have their stats at a different place. + for ((statName, statBuff) in namedReforgeStats) { + val statLine = StatLine(statName, null).addStat(statBuff, buffKind) + loreMut.add(statBlockLastIndex + 1, statLine.reconstitute()) + } + itemStack.loreAccordingToNbt = loreMut + } + + data class StatFormatting( + val postFix: String, + val color: Formatting, + ) + + val formattingOverrides = mapOf( + "Sea Creature Chance" to StatFormatting("%", Formatting.RED), + "Strength" to StatFormatting("", Formatting.RED), + "Damage" to StatFormatting("", Formatting.RED), + "Bonus Attack Speed" to StatFormatting("%", Formatting.RED), + "Shot Cooldown" to StatFormatting("s", Formatting.RED), + "Ability Damage" to StatFormatting("%", Formatting.RED), + "Crit Damage" to StatFormatting("%", Formatting.RED), + "Crit Chance" to StatFormatting("%", Formatting.RED), + "Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN), + // TODO: add other types and make this a repo json + ) + + + private val statLabelRegex = "(?.*): ".toPattern() + + enum class BuffKind( + val color: Formatting, + val prefix: String, + val postFix: String, + ) { + REFORGE(Formatting.BLUE, "(", ")"), + + ; + } + + data class StatLine( + val statName: String, + val value: Text?, + val rest: List = listOf(), + val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', '%', '+')?.toDoubleOrNull() + ) { + fun addStat(amount: Double, buffKind: BuffKind): StatLine { + val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true) + return copy( + valueNum = (valueNum ?: 0.0) + amount, + value = null, + rest = rest + + listOf( + Text.literal( + buffKind.prefix + formattedAmount + + statFormatting.postFix + + buffKind.postFix + " ") + .withColor(buffKind.color))) + } + + fun formatValue() = + Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0, 1, includeSign = true) + statFormatting.postFix + " ") + .setStyle(Style.EMPTY.withColor(statFormatting.color)) + + val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN) + fun reconstitute(): Text = + Text.literal("").setStyle(Style.EMPTY.withItalic(false)) + .append(Text.literal("$statName: ").grey()) + .append(value ?: formatValue()) + .also { rest.forEach(it::append) } + } + + private fun statIdToName(statId: String): String { + return statId.split("_").joinToString(" ") { + it.replaceFirstChar { it.uppercaseChar() } + } + } + + private fun parseStatLine(line: Text): StatLine? { + val sibs = line.siblings + val stat = sibs.firstOrNull() ?: return null + if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null + val statLabel = stat.directLiteralStringContent ?: return null + val statName = statLabelRegex.useMatch(statLabel) { group("statName") } ?: return null + return StatLine(statName, sibs[1], sibs.subList(2, sibs.size)) + } } constructor(skyblockId: SkyblockId, petData: PetData) : this( @@ -134,11 +254,19 @@ data class SBItemStack constructor( } - private fun appendReforgeStatsToLore( + private fun appendReforgeInfo( itemStack: ItemStack, ) { - val rarity = itemStack.rarity - val lore = itemStack.loreAccordingToNbt + val rarity = Rarity.fromItem(itemStack) ?: return + val reforgeId = this.reforge ?: return + val reforge = ReforgeStore.modifierLut[reforgeId] ?: return + val reforgeStats = reforge.reforgeStats?.get(rarity) ?: mapOf() + itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy() + .prepend(Text.literal(reforge.reforgeName + " ").formatted(Rarity.colourMap[rarity] ?: Formatting.WHITE)) + val data = itemStack.extraAttributes.copy() + data.putString("modifier", reforgeId.id) + itemStack.extraAttributes = data + appendEnhancedStats(itemStack, reforgeStats, BuffKind.REFORGE) } // TODO: avoid instantiating the item stack here @@ -156,8 +284,8 @@ data class SBItemStack constructor( injectReplacementDataForPets(replacementData) return@run neuItem.asItemStack(idHint = skyblockId, replacementData) .copyWithCount(stackSize) + .also { appendReforgeInfo(it) } .also { it.appendLore(extraLore) } - .also { if (reforge != null) it.appendLore(listOf(Text.literal("Reforge: $reforge"))) } // TODO: use this for proper rendering .also { enhanceStatsByStars(it, stars) } } if (itemStack_ == null) diff --git a/src/main/kotlin/util/AprilFoolsUtil.kt b/src/main/kotlin/util/AprilFoolsUtil.kt new file mode 100644 index 0000000..a940fa1 --- /dev/null +++ b/src/main/kotlin/util/AprilFoolsUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util + +import java.time.LocalDateTime +import java.time.Month + +object AprilFoolsUtil { + val isAprilFoolsDay = LocalDateTime.now().let { + it.dayOfMonth == 1 && it.month == Month.APRIL + } +} diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt index 92fb9e5..4b32c2a 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -15,21 +15,25 @@ import net.minecraft.text.Text object FirmFormatters { fun formatCommas(int: Int, segments: Int = 3): String = formatCommas(int.toLong(), segments) - fun formatCommas(long: Long, segments: Int = 3): String { + fun formatCommas(long: Long, segments: Int = 3, includeSign: Boolean = false): String { + if (long < 0 && long != Long.MIN_VALUE) { + return "-" + formatCommas(-long, segments, false) + } + val prefix = if (includeSign) "+" else "" val α = long / 1000 if (α != 0L) { - return formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0') + return prefix + formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0') } - return long.toString() + return prefix + long.toString() } fun formatCommas(float: Float, fractionalDigits: Int): String = formatCommas(float.toDouble(), fractionalDigits) - fun formatCommas(double: Double, fractionalDigits: Int): String { + fun formatCommas(double: Double, fractionalDigits: Int, includeSign: Boolean = false): String { val long = double.toLong() val δ = (double - long).absoluteValue val μ = pow(10, fractionalDigits) val digits = (μ * δ).toInt().toString().padStart(fractionalDigits, '0').trimEnd('0') - return formatCommas(long) + (if (digits.isEmpty()) "" else ".$digits") + return formatCommas(long, includeSign = includeSign) + (if (digits.isEmpty()) "" else ".$digits") } fun formatDistance(distance: Double): String { diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index 497a2d2..631b444 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -106,7 +106,10 @@ data class HypixelPetInfo( private val jsonparser = Json { ignoreUnknownKeys = true } -val ItemStack.extraAttributes: NbtCompound +var ItemStack.extraAttributes: NbtCompound + set(value) { + set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(value)) + } get() { val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run { val component = NbtComponent.of(NbtCompound()) diff --git a/src/main/kotlin/util/mc/NbtItemData.kt b/src/main/kotlin/util/mc/NbtItemData.kt index e8a908f..0c49862 100644 --- a/src/main/kotlin/util/mc/NbtItemData.kt +++ b/src/main/kotlin/util/mc/NbtItemData.kt @@ -5,8 +5,8 @@ import net.minecraft.component.type.LoreComponent import net.minecraft.item.ItemStack import net.minecraft.text.Text -var ItemStack.loreAccordingToNbt - get() = get(DataComponentTypes.LORE)?.lines ?: listOf() +var ItemStack.loreAccordingToNbt: List + get() = get(DataComponentTypes.LORE)?.lines ?: listOf() set(value) { set(DataComponentTypes.LORE, LoreComponent(value)) } diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt index 0244630..b19f371 100644 --- a/src/main/kotlin/util/skyblock/Rarity.kt +++ b/src/main/kotlin/util/skyblock/Rarity.kt @@ -8,7 +8,9 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import net.minecraft.item.ItemStack +import net.minecraft.text.Style import net.minecraft.text.Text +import net.minecraft.util.Formatting import moe.nea.firmament.util.StringUtil.words import moe.nea.firmament.util.collections.lastNotNullOfOrNull import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -46,10 +48,23 @@ enum class Rarity(vararg altNames: String) { } val names = setOf(name) + altNames - + val text: Text get() = Text.literal(name).setStyle(Style.EMPTY.withColor(colourMap[this])) val neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name } companion object { + // TODO: inline those formattings as fields + val colourMap = 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, + ) val byName = entries.flatMap { en -> en.names.map { it to en } }.toMap() val fromNeuRepo = entries.associateBy { it.neuRepoRarity } diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt index c94ebfe..cfd8429 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -7,4 +7,5 @@ object SkyBlockItems { val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND") val DIAMOND = SkyblockId("DIAMOND") val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE") + val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL") } diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 06ed8c8..ab3de43 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -133,6 +133,7 @@ fun MutableText.darkGreen() = withColor(Formatting.DARK_GREEN) fun MutableText.purple() = withColor(Formatting.DARK_PURPLE) fun MutableText.pink() = withColor(Formatting.LIGHT_PURPLE) fun MutableText.yellow() = withColor(Formatting.YELLOW) +fun MutableText.gold() = withColor(Formatting.GOLD) fun MutableText.grey() = withColor(Formatting.GRAY) fun MutableText.red() = withColor(Formatting.RED) fun MutableText.white() = withColor(Formatting.WHITE) @@ -146,6 +147,11 @@ fun MutableText.clickCommand(command: String): MutableText { } } +fun MutableText.prepend(text: Text): MutableText { + siblings.addFirst(text) + return this +} + fun Text.transformEachRecursively(function: (Text) -> Text): Text { val c = this.content if (c is TranslatableTextContent) { -- cgit