diff options
Diffstat (limited to 'src/main/kotlin/features')
25 files changed, 944 insertions, 93 deletions
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt index f85825b..a084234 100644 --- a/src/main/kotlin/features/chat/ChatLinks.kt +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -3,6 +3,7 @@ package moe.nea.firmament.features.chat import io.ktor.client.request.get import io.ktor.client.statement.bodyAsChannel import io.ktor.utils.io.jvm.javaio.toInputStream +import java.net.URI import java.net.URL import java.util.Collections import java.util.concurrent.atomic.AtomicInteger @@ -78,7 +79,7 @@ object ChatLinks : FirmamentFeature { val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") MC.textureManager.registerTexture( texId, - NativeImageBackedTexture(image) + NativeImageBackedTexture({ texId.path }, image) ) Image(texId, image.width, image.height) } else @@ -102,8 +103,8 @@ object ChatLinks : FirmamentFeature { if (it.screen !is ChatScreen) return val hoveredComponent = MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return - val hoverEvent = hoveredComponent.hoverEvent ?: return - val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return + val hoverEvent = hoveredComponent.hoverEvent as? HoverEvent.ShowText ?: return + val value = hoverEvent.value val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return if (!isImageUrl(url)) return val imageFuture = imageCache[url] ?: return @@ -149,8 +150,8 @@ object ChatLinks : FirmamentFeature { Text.literal(url).setStyle( Style.EMPTY.withUnderline(true).withColor( Formatting.AQUA - ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url))) - .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url)) + ).withHoverEvent(HoverEvent.ShowText(Text.literal(url))) + .withClickEvent(ClickEvent.OpenUrl(URI(url))) ) ) if (isImageUrl(url)) diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt index 11b47a9..9f9f135 100644 --- a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -1,51 +1,193 @@ package moe.nea.firmament.features.debug -import net.minecraft.component.DataComponentTypes +import net.minecraft.command.argument.RegistryKeyArgumentType +import net.minecraft.component.ComponentType import net.minecraft.entity.Entity +import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtOps +import net.minecraft.registry.RegistryKeys import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.commands.thenLiteral import moe.nea.firmament.events.CommandEvent import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC -import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.math.GChainReconciliation +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.NbtPrism import moe.nea.firmament.util.tr object AnimatedClothingScanner { - var observedEntity: Entity? = null + data class LensOfFashionTheft<T>( + val prism: NbtPrism, + val component: ComponentType<T>, + ) { + fun observe(itemStack: ItemStack): Collection<NbtElement> { + val x = itemStack.get(component) ?: return listOf() + val nbt = component.codecOrThrow.encodeStart(NbtOps.INSTANCE, x).orThrow + return prism.access(nbt) + } + } + + var lens: LensOfFashionTheft<*>? = null + var subject: Entity? = null + var history: MutableList<String> = mutableListOf() + val metaHistory: MutableList<List<String>> = mutableListOf() @OptIn(ExperimentalStdlibApi::class) @Subscribe fun onUpdate(event: EntityUpdateEvent) { - if (event.entity != observedEntity) return + val s = subject ?: return + if (event.entity != s) return + val l = lens ?: return if (event is EntityUpdateEvent.EquipmentUpdate) { event.newEquipment.forEach { - val id = it.second.skyBlockId?.neuItem - val colour = it.second.get(DataComponentTypes.DYED_COLOR) - ?.rgb?.toHexString(HexFormat.UpperCase) - ?.let { " #$it" } ?: "" - MC.sendChat(tr("firmament.fitstealer.update", - "[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => ${id}${colour}")) + val formatted = (l.observe(it.second)).joinToString() + history.add(formatted) + // TODO: add a slot filter } } } + fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> { + return metaHistory.fold(history, reducer).shortenCycle() + } + @Subscribe fun onSubCommand(event: CommandEvent.SubCommand) { event.subcommand("dev") { thenLiteral("stealthisfit") { - thenExecute { - observedEntity = - if (observedEntity == null) MC.instance.targetedEntity else null - - MC.sendChat( - observedEntity?.let { - tr("firmament.fitstealer.targeted", "Observing the equipment of ${it.name}.") - } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), - ) + thenLiteral("clear") { + thenExecute { + subject = null + metaHistory.clear() + history.clear() + MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history")) + } + } + thenLiteral("copy") { + thenExecute { + val history = reduceHistory { a, b -> a + b } + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history")) + } + thenLiteral("deduplicated") { + thenExecute { + val history = reduceHistory { a, b -> + (a.toMutableSet() + b).toList() + } + copyHistory(history) + MC.sendChat( + tr( + "firmament.fitstealer.copied.deduplicated", + "Copied the deduplicated history" + ) + ) + } + } + thenLiteral("merged") { + thenExecute { + val history = reduceHistory(GChainReconciliation::reconcileCycles) + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history")) + } + } + } + thenLiteral("target") { + thenLiteral("self") { + thenExecute { + toggleObserve(MC.player!!) + } + } + thenLiteral("pet") { + thenExecute { + source.sendFeedback( + tr( + "firmament.fitstealer.stealingpet", + "Observing nearest marker armourstand" + ) + ) + val p = MC.player!! + val nearestPet = p.world.getEntitiesByClass( + ArmorStandEntity::class.java, + p.boundingBox.expand(10.0), + { it.isMarker }) + .minBy { it.squaredDistanceTo(p) } + toggleObserve(nearestPet) + } + } + thenExecute { + val ent = MC.instance.targetedEntity + if (ent == null) { + source.sendFeedback( + tr( + "firmament.fitstealer.notargetundercursor", + "No entity under cursor" + ) + ) + } else { + toggleObserve(ent) + } + } + } + thenLiteral("path") { + thenArgument( + "component", + RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE) + ) { component -> + thenArgument("path", NbtPrism.Argument) { path -> + thenExecute { + lens = LensOfFashionTheft( + get(path), + MC.unsafeGetRegistryEntry(get(component))!!, + ) + source.sendFeedback( + tr( + "firmament.fitstealer.lensset", + "Analyzing path ${get(path)} for component ${get(component).value}" + ) + ) + } + } + } } } } } + + private fun copyHistory(toCopy: List<String>) { + ClipboardUtils.setTextContent(toCopy.joinToString("\n")) + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + subject = null + if (history.isNotEmpty()) { + metaHistory.add(history) + history = mutableListOf() + } + } + + private fun toggleObserve(entity: Entity?) { + subject = if (subject == null) entity else null + if (subject == null) { + metaHistory.add(history) + history = mutableListOf() + } + MC.sendChat( + subject?.let { + tr( + "firmament.fitstealer.targeted", + "Observing the equipment of ${it.name}." + ) + } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), + ) + } } diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt index 2c6b962..9115956 100644 --- a/src/main/kotlin/features/debug/DebugLogger.kt +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -10,6 +10,7 @@ class DebugLogger(val tag: String) { companion object { val allInstances = InstanceList<DebugLogger>("DebugLogger") } + object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf) init { @@ -17,6 +18,7 @@ class DebugLogger(val tag: String) { } fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag) + fun log(text: String) = log { text } fun log(text: () -> String) { if (!isEnabled()) return MC.sendChat(Text.literal(text())) diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt index 8f0c25c..af1e92e 100644 --- a/src/main/kotlin/features/debug/DeveloperFeatures.kt +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -3,6 +3,10 @@ package moe.nea.firmament.features.debug import java.io.File import java.nio.file.Path import java.util.concurrent.CompletableFuture +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.Mixin import kotlinx.serialization.json.encodeToStream import kotlin.io.path.absolute import kotlin.io.path.exists @@ -10,11 +14,14 @@ import net.minecraft.client.MinecraftClient import net.minecraft.text.Text import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.DebugInstantiateEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.init.MixinPlugin import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.asm.AsmAnnotationUtil import moe.nea.firmament.util.iterate object DeveloperFeatures : FirmamentFeature { @@ -42,6 +49,42 @@ object DeveloperFeatures : FirmamentFeature { } @Subscribe + fun loadAllMixinClasses(event: DebugInstantiateEvent) { + val allMixinClasses = mutableSetOf<String>() + MixinPlugin.instances.forEach { plugin -> + val prefix = plugin.mixinPackage + "." + val classes = plugin.mixins.map { prefix + it } + allMixinClasses.addAll(classes) + for (cls in classes) { + val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use { + val node = ClassNode() + ClassReader(it).accept(node, 0) + val mixins = mutableListOf<Mixin>() + (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach { + val annotationType = Type.getType(it.desc) + val mixinType = Type.getType(Mixin::class.java) + if (mixinType == annotationType) { + mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it)) + } + } + mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } } + } + for (target in targets) + try { + Firmament.logger.debug("Loading ${target} to force instantiate ${cls}") + Class.forName(target, true, javaClass.classLoader) + } catch (ex: Throwable) { + Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex) + } + } + } + Firmament.logger.info("Forceloaded all Firmament mixins:") + val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet() + applied.forEach { Firmament.logger.info(" - ${it}") } + require(allMixinClasses == applied) + } + + @Subscribe fun dumpMissingTranslations(tickEvent: TickEvent) { val toDump = missingTranslations ?: return missingTranslations = null diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt new file mode 100644 index 0000000..a817dd6 --- /dev/null +++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.debug + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional + +data class ExportedTestConstantMeta( + val dataVersion: Int, + val modVersion: Optional<String>, +) { + companion object { + val CODEC: Codec<ExportedTestConstantMeta> = RecordCodecBuilder.create { + it.group( + Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion), + Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion), + ).apply(it, ::ExportedTestConstantMeta) + } + } +} diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 8be5d5d..893b176 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -1,31 +1,25 @@ package moe.nea.firmament.features.debug -import com.mojang.serialization.Codec -import com.mojang.serialization.DynamicOps import com.mojang.serialization.JsonOps -import com.mojang.serialization.codecs.RecordCodecBuilder import kotlin.jvm.optionals.getOrNull import net.minecraft.block.SkullBlock import net.minecraft.block.entity.SkullBlockEntity import net.minecraft.component.DataComponentTypes import net.minecraft.component.type.ProfileComponent import net.minecraft.entity.Entity -import net.minecraft.entity.EntityType import net.minecraft.entity.LivingEntity import net.minecraft.item.ItemStack import net.minecraft.item.Items -import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtString import net.minecraft.predicate.NbtPredicate import net.minecraft.text.Text import net.minecraft.text.TextCodecs import net.minecraft.util.Identifier +import net.minecraft.util.Nameable import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult import net.minecraft.util.hit.HitResult -import net.minecraft.util.math.Position -import net.minecraft.util.math.Vec3d import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent @@ -43,8 +37,10 @@ import moe.nea.firmament.util.mc.IntrospectableItemModelManager import moe.nea.firmament.util.mc.SNbtFormatter import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.iterableArmorItems import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr object PowerUserTools : FirmamentFeature { override val identifier: String @@ -59,6 +55,7 @@ object PowerUserTools : FirmamentFeature { val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") val copyEntityData by keyBindingWithDefaultUnbound("entity-data") val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack") + val copyTitle by keyBindingWithDefaultUnbound("copy-title") } override val config @@ -108,7 +105,7 @@ object PowerUserTools : FirmamentFeature { MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) if (target is LivingEntity) { MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) - for (armorItem in target.armorItems) { + for ((slot, armorItem) in target.iterableArmorItems) { MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) } } @@ -179,11 +176,23 @@ object PowerUserTools : FirmamentFeature { Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())) println("Copied skull id: $skullTexture") } else if (it.matches(TConfig.copyItemStack)) { - ClipboardUtils.setTextContent( - ItemStack.CODEC - .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) - .orThrow.toPrettyString()) + val nbt = ItemStack.CODEC + .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) + .orThrow + ClipboardUtils.setTextContent(nbt.toPrettyString()) lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) + } else if (it.matches(TConfig.copyTitle)) { + val allTitles = NbtList() + val inventoryNames = + it.screen.screenHandler.slots + .mapNotNullTo(mutableSetOf()) { it.inventory } + .filterIsInstance<Nameable>() + .map { it.name } + for (it in listOf(it.screen.title) + inventoryNames) { + allTitles.add(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!) + } + ClipboardUtils.setTextContent(allTitles.toPrettyString()) + MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles")) } } diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt index 1824225..cfc05cc 100644 --- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt +++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt @@ -222,7 +222,7 @@ object MinesweeperHelper { fun onChat(event: ProcessChatEvent) { if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) { MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled { - it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial")) + it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial")) }) } if (!CarnivalFeatures.TConfig.enableBombSolver) { @@ -259,7 +259,7 @@ object MinesweeperHelper { val boardPosition = BoardPosition.fromBlockPos(event.blockPos) log.log { "Breaking block at ${event.blockPos} ($boardPosition)" } gs.lastClickedPosition = boardPosition - gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack) + gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandStack) } @Subscribe diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt index 3dae233..5e6350d 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -24,6 +24,7 @@ object Fixes : FirmamentFeature { val peekChat by keyBindingWithDefaultUnbound("peek-chat") val hidePotionEffects by toggle("hide-mob-effects") { false } val noHurtCam by toggle("disable-hurt-cam") { false } + val hideSlotHighlights by toggle("hide-slot-highlights") { false } } override val config: ManagedConfig @@ -50,7 +51,7 @@ object Fixes : FirmamentFeature { "firmament.fixes.auto-sprint.sprinting" else "firmament.fixes.auto-sprint.not-sprinting" - ), 0, 0, -1, false + ), 0, 0, -1, true ) it.context.matrices.pop() } diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt index 5ca10f7..bb39fbc 100644 --- a/src/main/kotlin/features/inventory/PetFeatures.kt +++ b/src/main/kotlin/features/inventory/PetFeatures.kt @@ -1,14 +1,24 @@ package moe.nea.firmament.features.inventory -import net.minecraft.util.Identifier +import moe.nea.jarvis.api.Point +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.FirmFormatters.formatPercent +import moe.nea.firmament.util.FirmFormatters.shortFormat import moe.nea.firmament.util.MC import moe.nea.firmament.util.petData import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.titleCase import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor object PetFeatures : FirmamentFeature { override val identifier: String @@ -19,9 +29,12 @@ object PetFeatures : FirmamentFeature { object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val highlightEquippedPet by toggle("highlight-pet") { true } + var petOverlay by toggle("pet-overlay") { false } + val petOverlayHud by position("pet-overlay-hud", 80, 10) { Point(0.5, 1.0) } } val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + var petItemStack: ItemStack? = null @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { @@ -29,12 +42,44 @@ object PetFeatures : FirmamentFeature { val stack = event.slot.stack if (stack.petData?.active == true) petMenuTitle.useMatch(MC.screenName ?: return) { - event.context.drawGuiTexture( - event.slot.x, event.slot.y, 0, 16, 16, - Identifier.of("firmament:selected_pet_background") - ) - } + petItemStack = stack + event.context.drawGuiTexture( + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, + ) + } } + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.petOverlay) return + val itemStack = petItemStack ?: return + val petData = petItemStack?.petData ?: return + val rarity = Rarity.fromNeuRepo(petData.tier) + val rarityCode = Rarity.colourMap[rarity] ?: Formatting.WHITE + val xp = petData.level + val petType = titleCase(petData.type) + val heldItem = petData.heldItem?.let { item -> "Held Item: ${titleCase(item)}" } + + it.context.matrices.push() + TConfig.petOverlayHud.applyTransformations(it.context.matrices) + + val lines = mutableListOf<Text>() + it.context.matrices.push() + it.context.matrices.translate(-0.5, -0.5, 0.0) + it.context.matrices.scale(2f, 2f, 1f) + it.context.drawItem(itemStack, 0, 0) + it.context.matrices.pop() + lines.add(Text.literal("[Lvl ${xp.currentLevel}] ").append(Text.literal(petType).withColor(rarityCode))) + if (heldItem != null) lines.add(Text.literal(heldItem)) + if (xp.currentLevel != xp.maxLevel) lines.add(Text.literal("Required L${xp.currentLevel + 1}: ${shortFormat(xp.expInCurrentLevel.toDouble())}/${shortFormat(xp.expRequiredForNextLevel.toDouble())} (${formatPercent(xp.percentageToNextLevel.toDouble())})")) + lines.add(Text.literal("Required L100: ${shortFormat(xp.expTotal.toDouble())}/${shortFormat(xp.expRequiredForMaxLevel.toDouble())} (${formatPercent(xp.percentageToMaxLevel.toDouble())})")) + + for ((index, line) in lines.withIndex()) { + it.context.drawText(MC.font, line.copy().withColor(Formatting.GRAY), 36, MC.font.fontHeight * index, -1, true) + } + + it.context.matrices.pop() + } } diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt index 4477203..a724d63 100644 --- a/src/main/kotlin/features/inventory/PriceData.kt +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.inventory import net.minecraft.text.Text @@ -8,44 +6,68 @@ import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.repo.HypixelStaticData -import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.FirmFormatters.formatCommas +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.gold import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.yellow object PriceData : FirmamentFeature { - override val identifier: String - get() = "price-data" + override val identifier: String + get() = "price-data" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val tooltipEnabled by toggle("enable-always") { true } + val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") + } - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val tooltipEnabled by toggle("enable-always") { true } - val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") - } + override val config get() = TConfig - override val config get() = TConfig + fun formatPrice(label: Text, price: Double): Text { + return Text.literal("") + .yellow() + .bold() + .append(label) + .append(": ") + .append( + Text.literal(formatCommas(price, fractionalDigits = 1)) + .append(if(price != 1.0) " coins" else " coin") + .gold() + .bold() + ) + } - @Subscribe - fun onItemTooltip(it: ItemTooltipEvent) { - if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) { - return - } - val sbId = it.stack.skyBlockId - val bazaarData = HypixelStaticData.bazaarData[sbId] - val lowestBin = HypixelStaticData.lowestBin[sbId] - if (bazaarData != null) { - it.lines.add(Text.literal("")) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order", - FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1)) - ) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order", - FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1)) - ) - } else if (lowestBin != null) { - it.lines.add(Text.literal("")) - it.lines.add( - Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin", - FirmFormatters.formatCommas(lowestBin, 1)) - ) - } - } + @Subscribe + fun onItemTooltip(it: ItemTooltipEvent) { + if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) { + return + } + val sbId = it.stack.skyBlockId + val bazaarData = HypixelStaticData.bazaarData[sbId] + val lowestBin = HypixelStaticData.lowestBin[sbId] + if (bazaarData != null) { + it.lines.add(Text.literal("")) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"), + bazaarData.quickStatus.sellPrice + ) + ) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"), + bazaarData.quickStatus.buyPrice + ) + ) + } else if (lowestBin != null) { + it.lines.add(Text.literal("")) + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"), + lowestBin + ) + ) + } + } } diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt index 1e9b1b8..7d88dd1 100644 --- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt +++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt @@ -1,5 +1,6 @@ package moe.nea.firmament.features.inventory +import java.net.URI import net.fabricmc.loader.api.FabricLoader import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -38,7 +39,7 @@ object REIDependencyWarner { .white() .append(Text.literal("[").aqua()) .append(Text.translatable("firmament.download", modName) - .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) } + .styled { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) } .yellow() .also { if (alreadyDownloaded) diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index 0083c40..d3348a2 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,6 +4,7 @@ package moe.nea.firmament.features.inventory import java.util.UUID import org.lwjgl.glfw.GLFW +import util.render.CustomRenderLayers import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers @@ -17,6 +18,9 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.int import kotlinx.serialization.serializer import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.render.TexturedRenderLayers import net.minecraft.entity.player.PlayerInventory import net.minecraft.screen.GenericContainerScreenHandler import net.minecraft.screen.slot.Slot @@ -45,7 +49,6 @@ import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt -import moe.nea.firmament.util.render.GuiRenderLayers import moe.nea.firmament.util.render.drawLine import moe.nea.firmament.util.skyblock.DungeonUtil import moe.nea.firmament.util.skyblockUUID @@ -445,7 +448,7 @@ object SlotLocking : FirmamentFeature { val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf()) if (isSlotLocked || isUUIDLocked) { it.context.drawGuiTexture( - GuiRenderLayers.GUI_TEXTURED_NO_DEPTH, + RenderLayer::getGuiTexturedOverlay, when { isSlotLocked -> (Identifier.of("firmament:slot_locked")) diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt index 92640c8..361e8d0 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -5,16 +5,22 @@ package moe.nea.firmament.features.inventory.buttons import me.shedaniel.math.Rectangle import kotlinx.serialization.Serializable import kotlinx.serialization.serializer +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.MinecraftClient +import net.minecraft.text.Text import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HandledScreenClickEvent import moe.nea.firmament.events.HandledScreenForegroundEvent import moe.nea.firmament.events.HandledScreenPushREIEvent import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.FirmHoverComponent import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.MC import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.data.DataHolder import moe.nea.firmament.util.accessors.getRectangle +import moe.nea.firmament.util.gold object InventoryButtons : FirmamentFeature { override val identifier: String @@ -24,6 +30,7 @@ object InventoryButtons : FirmamentFeature { val _openEditor by button("open-editor") { openEditor() } + val hoverText by toggle("hover-text") { true } } object DConfig : DataHolder<Data>(serializer(), identifier, ::Data) @@ -60,16 +67,36 @@ object InventoryButtons : FirmamentFeature { } } + var lastHoveredComponent: InventoryButton? = null + var lastMouseMove = TimeMark.farPast() + @Subscribe fun onRenderForeground(it: HandledScreenForegroundEvent) { val bounds = it.screen.getRectangle() + + var hoveredComponent: InventoryButton? = null for (button in getValidButtons()) { val buttonBounds = button.getBounds(bounds) it.context.matrices.push() it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F) button.render(it.context) it.context.matrices.pop() + + if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) { + hoveredComponent = button + if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) { + it.context.drawTooltip( + MC.font, + listOf(Text.literal(button.command).gold()), + buttonBounds.minX - 15, + buttonBounds.maxY + 20, + ) + } + } } + if (hoveredComponent !== lastHoveredComponent) + lastMouseMove = TimeMark.now() + lastHoveredComponent = hoveredComponent lastRectangle = bounds } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt index 8fad4df..d7346c2 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -32,8 +32,8 @@ sealed interface StorageBackingHandle { StorageBackingHandle, HasBackingScreen companion object { - private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex() - private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex() + private val enderChestName = "^Ender Chest (?:✦ )?\\(([1-9])/[1-9]\\)$".toRegex() + private val backPackName = "^.+Backpack (?:✦ )?\\(Slot #([0-9]+)\\)$".toRegex() /** * Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt index 2e807de..2101915 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -33,6 +33,7 @@ object StorageOverlay : FirmamentFeature { val inverseScroll by toggle("inverse-scroll") { false } val padding by integer("padding", 1, 20) { 5 } val margin by integer("margin", 1, 60) { 20 } + val itemsBlockScrolling by toggle("block-item-scrolling") { true } } fun adjustScrollSpeed(amount: Double): Double { diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt index 6092e26..81f058e 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -9,6 +9,7 @@ import net.minecraft.entity.player.PlayerInventory import net.minecraft.screen.slot.Slot import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.focusedItemStack class StorageOverlayCustom( val handler: StorageBackingHandle, @@ -113,6 +114,8 @@ class StorageOverlayCustom( horizontalAmount: Double, verticalAmount: Double ): Boolean { + if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling) + return false return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt index 63a2f54..22f4dab 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -218,7 +218,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - val items = MC.player?.inventory?.main ?: return + val items = MC.player?.inventory?.mainStacks ?: return items.withIndex().forEach { (index, item) -> val (x, y) = getPlayerInventorySlotPosition(index) context.drawItem(item, x, y, 0) diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index 3b86184..d99acd7 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlin.jvm.optionals.getOrNull import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtIo @@ -42,15 +43,15 @@ data class VirtualInventory( 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 items = n.getList(INVENTORY).getOrNull() val ops = getOps() - return VirtualInventory(items.map { + 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 } - }) + } ?: listOf()) } fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt new file mode 100644 index 0000000..5c5ac0e --- /dev/null +++ b/src/main/kotlin/features/macros/ComboProcessor.kt @@ -0,0 +1,114 @@ +package moe.nea.firmament.features.macros + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.util.InputUtil +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr + +object ComboProcessor { + + var rootTrie: Branch = Branch(mapOf()) + private set + + var activeTrie: Branch = rootTrie + private set + + var isInputting = false + var lastInput = TimeMark.farPast() + val breadCrumbs = mutableListOf<SavedKeyBinding>() + + init { + val f = SavedKeyBinding(InputUtil.GLFW_KEY_F) + val one = SavedKeyBinding(InputUtil.GLFW_KEY_1) + val two = SavedKeyBinding(InputUtil.GLFW_KEY_2) + setActions( + MacroData.DConfig.data.comboActions + ) + } + + fun setActions(actions: List<ComboKeyAction>) { + rootTrie = KeyComboTrie.fromComboList(actions) + reset() + } + + fun reset() { + activeTrie = rootTrie + lastInput = TimeMark.now() + isInputting = false + breadCrumbs.clear() + } + + @Subscribe + fun onTick(event: TickEvent) { + if (isInputting && lastInput.passedTime() > 3.seconds) + reset() + } + + + @Subscribe + fun onRender(event: HudRenderEvent) { + if (!isInputting) return + if (!event.isRenderingHud) return + event.context.matrices.push() + val width = 120 + event.context.matrices.translate( + (MC.window.scaledWidth - width) / 2F, + (MC.window.scaledHeight) / 2F + 8, + 0F + ) + val breadCrumbText = breadCrumbs.joinToString(" > ") + event.context.drawText( + MC.font, + tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText), + 0, + 0, + -1, + true + ) + event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F) + for ((key, value) in activeTrie.nodes) { + event.context.drawText( + MC.font, + Text.literal("$breadCrumbText > $key: ").append(value.label), + 0, + 0, + -1, + true + ) + event.context.matrices.translate(0F, MC.font.fontHeight + 1F, 0F) + } + event.context.matrices.pop() + } + + @Subscribe + fun onKeyBinding(event: WorldKeyboardEvent) { + val nextEntry = activeTrie.nodes.entries + .find { event.matches(it.key) } + if (nextEntry == null) { + reset() + return + } + event.cancel() + breadCrumbs.add(nextEntry.key) + lastInput = TimeMark.now() + isInputting = true + val value = nextEntry.value + when (value) { + is Branch -> { + activeTrie = value + } + + is Leaf -> { + value.execute() + reset() + } + }.let { } + } +} diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt new file mode 100644 index 0000000..011f797 --- /dev/null +++ b/src/main/kotlin/features/macros/HotkeyAction.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import moe.nea.firmament.util.MC + +@Serializable +sealed interface HotkeyAction { + // TODO: execute + val label: Text + fun execute() +} + +@Serializable +@SerialName("command") +data class CommandAction(val command: String) : HotkeyAction { + override val label: Text + get() = Text.literal("/$command") + + override fun execute() { + MC.sendCommand(command) + } +} + +// Mit onscreen anzeige: +// F -> 1 /equipment +// F -> 2 /wardrobe +// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße) + +// Radial menu +// Hold F +// Weight (mach eins doppelt so groß) +// /equipment +// /wardrobe + +// Bei allen: Filter! +// - Nur in Dungeons / andere Insel +// - Nur wenn ich Item X im inventar habe (fishing rod) + diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt new file mode 100644 index 0000000..57ff289 --- /dev/null +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -0,0 +1,68 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil + +sealed interface KeyComboTrie { + val label: Text + + companion object { + fun fromComboList( + combos: List<ComboKeyAction>, + ): Branch { + val root = Branch(mutableMapOf()) + for (combo in combos) { + var p = root + if (combo.keys.isEmpty()) { + ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty") + continue + } + for ((index, key) in combo.keys.withIndex()) { + val m = (p.nodes as MutableMap) + if (index == combo.keys.lastIndex) { + if (key in m) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keys.joinToString(" > ")} (another action ${m[key]} already exists).") + break + } + + m[key] = Leaf(combo.action) + } else { + val c = m.getOrPut(key) { Branch(mutableMapOf()) } + if (c !is Branch) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already") + break + } else { + p = c + } + } + } + } + return root + } + } +} + + +@Serializable +data class ComboKeyAction( + val action: HotkeyAction, + val keys: List<SavedKeyBinding>, +) + +data class Leaf(val action: HotkeyAction) : KeyComboTrie { + override val label: Text + get() = action.label + + fun execute() { + action.execute() + } +} + +data class Branch( + val nodes: Map<SavedKeyBinding, KeyComboTrie> +) : KeyComboTrie { + override val label: Text + get() = Text.literal("...") // TODO: better labels +} diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt new file mode 100644 index 0000000..78a5948 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import moe.nea.firmament.util.data.DataHolder + +@Serializable +data class MacroData( + var comboActions: List<ComboKeyAction> = listOf(), +){ + object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) +} diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt new file mode 100644 index 0000000..17fdd0a --- /dev/null +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -0,0 +1,161 @@ +package moe.nea.firmament.features.macros + +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList +import moe.nea.firmament.gui.config.KeyBindingStateManager +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil + +class MacroUI { + + + companion object { + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + // TODO: add button in config + event.subcommand("macros") { + thenExecute { + ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null)) + } + } + } + + } + + @field:Bind("combos") + val combos = Combos() + + class Combos { + @field:Bind("actions") + val actions: ObservableList<ActionEditor> = ObservableList( + MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { + ActionEditor(it, this) + } + ) + + var dontSave = false + + @Bind + fun beforeClose(): CloseEventListener.CloseAction { + if (!dontSave) + save() + return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE + } + + @Bind + fun addCommand() { + actions.add( + ActionEditor( + ComboKeyAction( + CommandAction("ac Hello from a Firmament Hotkey"), + listOf() + ), + this + ) + ) + } + + @Bind + fun discard() { + dontSave = true + MC.screen?.close() + } + + @Bind + fun saveAndClose() { + save() + MC.screen?.close() + } + + @Bind + fun save() { + MacroData.DConfig.data.comboActions = actions.map { it.asSaveable() } + MacroData.DConfig.markDirty() + ComboProcessor.setActions(MacroData.DConfig.data.comboActions) // TODO: automatically reload those from the config on startup + } + } + + class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) { + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + @Bind + fun delete() { + parent.combo.removeIf { it === this } + parent.combo.update() + } + + fun requestFocus() { + button.requestFocus() + } + } + + class ActionEditor(val action: ComboKeyAction, val parent: MacroUI.Combos) { + fun asSaveable(): ComboKeyAction { + return ComboKeyAction( + CommandAction(command), + combo.map { it.binding } + ) + } + + @field:Bind("command") + var command: String = (action.action as CommandAction).command + + @field:Bind("combo") + val combo = action.keys.map { KeyBindingEditor(it, this) }.toObservableList() + + @Bind + fun formattedCombo() = + combo.joinToString(" > ") { it.binding.toString() } + + @Bind + fun addStep() { + combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this)) + } + + @Bind + fun back() { + MC.screen?.close() + } + + @Bind + fun delete() { + parent.actions.removeIf { it === this } + parent.actions.update() + } + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor", this, MC.screen) + } + } +} + +private fun <T> ObservableList<T>.setAll(ts: Collection<T>) { + val observer = this.observer + this.clear() + this.addAll(ts) + this.observer = observer + this.update() +} diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt index 1737969..d39694a 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -1,9 +1,13 @@ package moe.nea.firmament.features.mining import java.util.regex.Pattern +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.MinecraftClient +import net.minecraft.client.toast.SystemToast import net.minecraft.item.ItemStack +import net.minecraft.text.Text import net.minecraft.util.DyeColor import net.minecraft.util.Hand import net.minecraft.util.Identifier @@ -47,6 +51,7 @@ object PickaxeAbility : FirmamentFeature { object TConfig : ManagedConfig(identifier, Category.MINING) { val cooldownEnabled by toggle("ability-cooldown") { false } val cooldownScale by integer("ability-scale", 16, 64) { 16 } + val cooldownReadyToast by toggle("ability-cooldown-toast") { false } val drillFuelBar by toggle("fuel-bar") { true } val blockOnPrivateIsland by choice( "block-on-dynamic", @@ -140,8 +145,7 @@ object PickaxeAbility : FirmamentFeature { } } ?: return val extra = it.item.extraAttributes - if (!extra.contains("drill_fuel")) return - val fuel = extra.getInt("drill_fuel") + val fuel = extra.getInt("drill_fuel").getOrNull() ?: return val percentage = fuel / maxFuel.toFloat() it.barOverride = DurabilityBarEvent.DurabilityBar( lerp( @@ -170,6 +174,11 @@ object PickaxeAbility : FirmamentFeature { nowAvailable.useMatch(it.unformattedString) { val ability = group("name") lastUsage[ability] = TimeMark.farPast() + if (!TConfig.cooldownReadyToast) return + val mc: MinecraftClient = MinecraftClient.getInstance() + mc.toastManager.add( + SystemToast.create(mc, SystemToast.Type.NARRATOR_TOGGLE, tr("firmament.pickaxe.ability-ready","Pickaxe Cooldown"), tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!")) + ) } } diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt new file mode 100644 index 0000000..4219177 --- /dev/null +++ b/src/main/kotlin/features/misc/LicenseViewer.kt @@ -0,0 +1,128 @@ +package moe.nea.firmament.features.misc + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.decodeFromStream +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.tr + +object LicenseViewer { + @Serializable + data class Software( + val licenses: List<License> = listOf(), + val webPresence: String? = null, + val projectName: String, + val projectDescription: String? = null, + val developers: List<Developer> = listOf(), + ) { + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = webPresence ?: "<no web presence>" + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun projectName() = projectName + + @Bind + fun projectDescription() = projectDescription ?: "<no project description>" + + @get:Bind("developers") + @Transient + val developersO = ObservableList(developers) + + @get:Bind("licenses") + @Transient + val licenses0 = ObservableList(licenses) + } + + @Serializable + data class Developer( + @get:Bind("name") val name: String, + val webPresence: String? = null + ) { + + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = webPresence ?: "<no web presence>" + } + + @Serializable + data class License( + @get:Bind("name") val licenseName: String, + val licenseUrl: String? = null + ) { + @Bind + fun open() { + MC.openUrl(licenseUrl ?: return) + } + + @Bind + fun hasUrl() = licenseUrl != null + + @Bind + fun url() = licenseUrl ?: "<no link to license text>" + } + + data class LicenseList( + val softwares: List<Software> + ) { + @get:Bind("softwares") + val obs = ObservableList(softwares) + } + + @OptIn(ExperimentalSerializationApi::class) + val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") { + Firmament.json.decodeFromStream<List<Software>?>( + javaClass.getResourceAsStream("/LICENSES-FIRMAMENT.json") ?: error("Could not find LICENSES-FIRMAMENT.json") + )?.let { LicenseList(it) } + }.orNull() + + fun showLicenses() { + ErrorUtil.catch("Could not display licenses") { + ScreenUtil.setScreenLater( + MoulConfigUtils.loadScreen( + "license_viewer/index", licenses!!, null + ) + ) + }.or { + MC.sendChat( + tr( + "firmament.licenses.notfound", + "Could not load licenses. Please check the Firmament source code for information directly." + ) + ) + } + } + + @Subscribe + fun onSubcommand(event: CommandEvent.SubCommand) { + event.subcommand("licenses") { + thenExecute { + showLicenses() + } + } + } +} |