diff options
Diffstat (limited to 'src/main/kotlin/features')
34 files changed, 1744 insertions, 377 deletions
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 9a3cbf8..f0c1857 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -31,89 +31,96 @@ import moe.nea.firmament.features.mining.PickaxeAbility import moe.nea.firmament.features.mining.PristineProfitTracker import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.features.world.Waypoints +import moe.nea.firmament.util.compatloader.ICompatMeta import moe.nea.firmament.util.data.DataHolder object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) { - @Serializable - data class Config( - val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() - ) + @Serializable + data class Config( + val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() + ) - private val features = mutableMapOf<String, FirmamentFeature>() + private val features = mutableMapOf<String, FirmamentFeature>() - val allFeatures: Collection<FirmamentFeature> get() = features.values + val allFeatures: Collection<FirmamentFeature> get() = features.values - private var hasAutoloaded = false + private var hasAutoloaded = false - fun autoload() { - synchronized(this) { - if (hasAutoloaded) return - loadFeature(MinorTrolling) - loadFeature(FairySouls) - loadFeature(AutoCompletions) - // TODO: loadFeature(FishingWarning) - loadFeature(SlotLocking) - loadFeature(StorageOverlay) - loadFeature(PristineProfitTracker) - loadFeature(CraftingOverlay) - loadFeature(PowerUserTools) - loadFeature(Waypoints) - loadFeature(ChatLinks) - loadFeature(InventoryButtons) - loadFeature(CompatibliltyFeatures) - loadFeature(AnniversaryFeatures) - loadFeature(QuickCommands) - loadFeature(PetFeatures) - loadFeature(SaveCursorPosition) - loadFeature(PriceData) - loadFeature(Fixes) - loadFeature(DianaWaypoints) - loadFeature(ItemRarityCosmetics) - loadFeature(PickaxeAbility) - loadFeature(CarnivalFeatures) - if (Firmament.DEBUG) { - loadFeature(DeveloperFeatures) - loadFeature(DebugView) - } - allFeatures.forEach { it.config } - FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) - hasAutoloaded = true - } - } + fun autoload() { + synchronized(this) { + if (hasAutoloaded) return + loadFeature(MinorTrolling) + loadFeature(FairySouls) + loadFeature(AutoCompletions) + // TODO: loadFeature(FishingWarning) + loadFeature(SlotLocking) + loadFeature(StorageOverlay) + loadFeature(PristineProfitTracker) + loadFeature(CraftingOverlay) + loadFeature(PowerUserTools) + loadFeature(Waypoints) + loadFeature(ChatLinks) + loadFeature(InventoryButtons) + loadFeature(CompatibliltyFeatures) + loadFeature(AnniversaryFeatures) + loadFeature(QuickCommands) + loadFeature(PetFeatures) + loadFeature(SaveCursorPosition) + loadFeature(PriceData) + loadFeature(Fixes) + loadFeature(DianaWaypoints) + loadFeature(ItemRarityCosmetics) + loadFeature(PickaxeAbility) + loadFeature(CarnivalFeatures) + if (Firmament.DEBUG) { + loadFeature(DeveloperFeatures) + loadFeature(DebugView) + } + allFeatures.forEach { it.config } + FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) + hasAutoloaded = true + } + } - fun subscribeEvents() { - SubscriptionList.allLists.forEach { - it.provideSubscriptions { - it.owner.javaClass.classes.forEach { - runCatching { it.getDeclaredField("INSTANCE").get(null) } + fun subscribeEvents() { + SubscriptionList.allLists.forEach { list -> + if (ICompatMeta.shouldLoad(list.javaClass.name)) + runCatching { + list.provideSubscriptions { + it.owner.javaClass.classes.forEach { + runCatching { it.getDeclaredField("INSTANCE").get(null) } + } + subscribeSingleEvent(it) + } + }.getOrElse { + // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically + Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it) } - subscribeSingleEvent(it) - } - } - } + } + } - private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { - it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) - } + private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { + it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) + } - fun loadFeature(feature: FirmamentFeature) { - synchronized(features) { - if (feature.identifier in features) { - Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") - return - } - features[feature.identifier] = feature - feature.onLoad() - } - } + fun loadFeature(feature: FirmamentFeature) { + synchronized(features) { + if (feature.identifier in features) { + Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") + return + } + features[feature.identifier] = feature + feature.onLoad() + } + } - fun isEnabled(identifier: String): Boolean? = - data.enabledFeatures[identifier] + fun isEnabled(identifier: String): Boolean? = + data.enabledFeatures[identifier] - fun setEnabled(identifier: String, value: Boolean) { - data.enabledFeatures[identifier] = value - markDirty() - } + fun setEnabled(identifier: String, value: Boolean) { + data.enabledFeatures[identifier] = value + markDirty() + } } 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 new file mode 100644 index 0000000..9f9f135 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,193 @@ +package moe.nea.firmament.features.debug + +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.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 { + + 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) { + val s = subject ?: return + if (event.entity != s) return + val l = lens ?: return + if (event is EntityUpdateEvent.EquipmentUpdate) { + event.newEquipment.forEach { + 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") { + 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 7030319..5e6350d 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -23,6 +23,8 @@ object Fixes : FirmamentFeature { 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 } + val noHurtCam by toggle("disable-hurt-cam") { false } + val hideSlotHighlights by toggle("hide-slot-highlights") { false } } override val config: ManagedConfig @@ -49,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 7a3a152..d3348a2 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,10 +4,23 @@ 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 +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +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 @@ -36,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 @@ -51,9 +63,66 @@ object SlotLocking : FirmamentFeature { val lockedSlots: MutableSet<Int> = mutableSetOf(), val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), - val boundSlots: MutableMap<Int, Int> = mutableMapOf() + val boundSlots: BoundSlots = BoundSlots() ) + @Serializable + data class BoundSlot( + val hotbar: Int, + val inventory: Int, + ) + + @Serializable(with = BoundSlots.Serializer::class) + data class BoundSlots( + val pairs: MutableSet<BoundSlot> = mutableSetOf() + ) { + fun findMatchingSlots(index: Int): List<BoundSlot> { + return pairs.filter { it.hotbar == index || it.inventory == index } + } + + fun removeDuplicateForInventory(index: Int) { + pairs.removeIf { it.inventory == index } + } + + fun removeAllInvolving(index: Int): Boolean { + return pairs.removeIf { it.inventory == index || it.hotbar == index } + } + + fun insert(hotbar: Int, inventory: Int) { + if (!TConfig.allowMultiBinding) { + removeAllInvolving(hotbar) + removeAllInvolving(inventory) + } + pairs.add(BoundSlot(hotbar, inventory)) + } + + object Serializer : KSerializer<BoundSlots> { + override val descriptor: SerialDescriptor + get() = serializer<JsonElement>().descriptor + + override fun serialize( + encoder: Encoder, + value: BoundSlots + ) { + serializer<MutableSet<BoundSlot>>() + .serialize(encoder, value.pairs) + } + + override fun deserialize(decoder: Decoder): BoundSlots { + decoder as JsonDecoder + val json = decoder.decodeJsonElement() + if (json is JsonObject) { + return BoundSlots(json.entries.map { + BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int) + }.toMutableSet()) + } + return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json)) + + } + } + } + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { @@ -62,6 +131,7 @@ 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 } + val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } } @@ -177,19 +247,19 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { - val boundSlots = DConfig.data?.boundSlots ?: mapOf() + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() val isValidAction = it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return val handler = MC.handledScreen?.screenHandler ?: return val slot = it.slot if (slot != null && it.slot.inventory is PlayerInventory) { - val boundSlot = boundSlots.entries.find { - it.value == slot.index || it.key == slot.index - } ?: return + val matchingSlots = boundSlots.findMatchingSlots(slot.index) + if (matchingSlots.isEmpty()) return it.protectSilent() - val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true) - inventorySlot?.swapWithHotBar(handler, boundSlot.key) + val boundSlot = matchingSlots.singleOrNull() ?: return + val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) + inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) } } @@ -228,10 +298,8 @@ object SlotLocking : FirmamentFeature { val boundSlots = DConfig.data?.boundSlots ?: return lockedSlots?.remove(hotBarSlot.index) lockedSlots?.remove(invSlot.index) - boundSlots.entries.removeIf { - it.value == invSlot.index - } - boundSlots[hotBarSlot.index] = invSlot.index + boundSlots.removeDuplicateForInventory(invSlot.index) + boundSlots.insert(hotBarSlot.index, invSlot.index) DConfig.markDirty() CommonSoundEffects.playSuccess() return @@ -245,9 +313,7 @@ object SlotLocking : FirmamentFeature { storedLockingSlot = null val boundSlots = DConfig.data?.boundSlots ?: return if (slot != null) - boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - } + boundSlots.removeAllInvolving(slot.index) } } @@ -258,9 +324,10 @@ object SlotLocking : FirmamentFeature { val accScreen = event.screen as AccessorHandledScreen val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament - for (it in boundSlots.entries) { - val hotbarSlot = findByIndex(it.key) ?: continue - val inventorySlot = findByIndex(it.value) ?: continue + val highlitSlots = mutableSetOf<Slot>() + for (it in boundSlots.pairs) { + val hotbarSlot = findByIndex(it.hotbar) ?: continue + val inventorySlot = findByIndex(it.inventory) ?: continue val (hotX, hotY) = hotbarSlot.lineCenter() val (invX, invY) = inventorySlot.lineCenter() @@ -268,22 +335,27 @@ object SlotLocking : FirmamentFeature { || 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 (anyHovered) { + highlitSlots.add(hotbarSlot) + highlitSlots.add(inventorySlot) + } + fun color(highlit: Boolean) = + if (highlit) + 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 + color(anyHovered) ) event.context.drawBorder(hotbarSlot.x + sx, hotbarSlot.y + sy, - 16, 16, color.color) + 16, 16, color(hotbarSlot in highlitSlots).color) event.context.drawBorder(inventorySlot.x + sx, inventorySlot.y + sy, - 16, 16, color.color) + 16, 16, color(inventorySlot in highlitSlots).color) } } @@ -339,11 +411,9 @@ object SlotLocking : FirmamentFeature { fun toggleSlotLock(slot: Slot) { val lockedSlots = lockedSlots ?: return - val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf() + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() if (slot.inventory is PlayerInventory) { - if (boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - }) { + if (boundSlots.removeAllInvolving(slot.index)) { // intentionally do nothing } else if (slot.index in lockedSlots) { lockedSlots.remove(slot.index) @@ -378,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/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt index f1b77c6..309ea61 100644 --- a/src/main/kotlin/features/inventory/TimerInLore.kt +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -80,7 +80,8 @@ object TimerInLore { COMMUNITYPROJECTS("Contribute again", "Come back at"), CHOCOLATEFACTORY("Next Charge", "Available at"), STONKSAUCTION("Auction ends in", "Ends at"), - LIZSTONKREDEMPTION("Resets in:", "Resets at"); + LIZSTONKREDEMPTION("Resets in:", "Resets at"), + EVENTENDING("Event ends in:", "Ends at"); } val regex = diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt index ee3ae8b..c4ea519 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -6,6 +6,7 @@ import io.github.notenoughupdates.moulconfig.xml.Bind import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import org.lwjgl.glfw.GLFW +import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.util.InputUtil @@ -55,6 +56,11 @@ class InventoryButtonEditor( super.close() } + override fun resize(client: MinecraftClient, width: Int, height: Int) { + lastGuiRect.move(MC.window.scaledWidth / 2 - lastGuiRect.width / 2, MC.window.scaledHeight / 2 - lastGuiRect.height / 2) + super.resize(client, width, height) + } + override fun init() { super.init() addDrawableChild( @@ -114,6 +120,13 @@ class InventoryButtonEditor( return newButtons } + override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + context.matrices.push() + context.matrices.translate(0F, 0F, -15F) + super.renderBackground(context, mouseX, mouseY, delta) + context.matrices.pop() + } + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) context.matrices.push() diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt index d5b5417..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 } @@ -78,9 +105,9 @@ object InventoryButtons : FirmamentFeature { ScreenUtil.setScreenLater( InventoryButtonEditor( lastRectangle ?: Rectangle( - MC.window.scaledWidth / 2 - 100, - MC.window.scaledHeight / 2 - 100, - 200, 200, + MC.window.scaledWidth / 2 - 88, + MC.window.scaledHeight / 2 - 83, + 176, 166, ) ) ) 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 633a8fe..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) @@ -357,6 +357,10 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return super.keyReleased(keyCode, scanCode, modifiers) } + override fun shouldCloseOnEsc(): Boolean { + return this === MC.screen // Fixes this UI closing the handled screen on Escape press. + } + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { if (typeMCComponentInPlace( controlComponent, 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/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt new file mode 100644 index 0000000..e8ea4f4 --- /dev/null +++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt @@ -0,0 +1,54 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.observer.Property +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screen.Screen +import net.minecraft.item.ItemStack +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.SkyBlockIsland + +object MiningBlockInfoUi { + class MiningInfo(miningData: MiningRepoData) { + @field:Bind("search") + @JvmField + var search = "" + + @get:Bind("ores") + val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) } + } + + class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) { + @get:Bind("oreName") + val oreName = block.name ?: "No Name" + + @get:Bind("blocks") + val res = ObservableList(block.blocks189.map { BlockInfo(it, info) }) + } + + class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) { + @get:Bind("item") + val item = ModernItemStack.of(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY) + + @get:Bind("isSelected") + val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) } + + @get:Bind("itemName") + val itemName get() = item.getDisplayName() + + @get:Bind("restrictions") + val res = ObservableList( + if (block.onlyIn != null) + block.onlyIn.map { " §r- §a${it.userFriendlyName}" } + else + listOf("Everywhere") + ) + } + + fun makeScreen(): Screen { + return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null) + } +} 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() + } + } + } +} diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt new file mode 100644 index 0000000..b92a91e --- /dev/null +++ b/src/main/kotlin/features/world/ColeWeightCompat.kt @@ -0,0 +1,125 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.tr + +object ColeWeightCompat { + @Serializable + data class ColeWeightWaypoint( + val x: Int, + val y: Int, + val z: Int, + val r: Int = 0, + val g: Int = 0, + val b: Int = 0, + ) + + fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> { + return waypoints.waypoints.map { + ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z) + } + } + + fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints { + val w = waypoints.map { + FirmWaypoints.Waypoint(it.x + relativeTo.x, it.y + relativeTo.y, it.z + relativeTo.z) + } + return FirmWaypoints( + "Imported Waypoints", + "imported", + null, + w.toMutableList(), + false + ) + } + + fun copyAndInform( + source: DefaultSource, + origin: BlockPos, + positiveFeedback: (Int) -> Text, + ) { + val waypoints = Waypoints.useNonEmptyWaypoints() + ?.let { fromFirm(it, origin) } + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return + } + val data = + Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + fun importAndInform( + source: DefaultSource, + pos: BlockPos?, + positiveFeedback: (Int) -> Text + ) { + val text = ClipboardUtils.getTextContents() + val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) } + val waypoints = wr.getOrElse { + source.sendError( + tr("firmament.command.waypoint.import.cw.error", + "Could not import ColeWeight waypoints.")) + Firmament.logger.error(it) + return + } + waypoints.lastRelativeImport = pos + Waypoints.waypoints = waypoints + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + @Subscribe + fun onEvent(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("exportcw") { + thenExecute { + copyAndInform(source, BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw", + "Copied $it waypoints to clipboard in ColeWeight format.") + } + } + } + thenLiteral("exportrelativecw") { + thenExecute { + copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw.relative", + "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.") + } + } + } + thenLiteral("importcw") { + thenExecute { + importAndInform(source, null) { + Text.stringifiedTranslatable("firmament.command.waypoint.import.cw", + it) + } + } + } + thenLiteral("importrelativecw") { + thenExecute { + importAndInform(source, MC.player!!.blockPos) { + tr("firmament.command.waypoint.import.cw.relative", + "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.") + } + } + } + } + } + + fun tryParse(string: String): Result<List<ColeWeightWaypoint>> { + return runCatching { + Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string) + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt new file mode 100644 index 0000000..d18483c --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypointManager.kt @@ -0,0 +1,168 @@ +package moe.nea.firmament.features.world + +import com.mojang.brigadier.arguments.StringArgumentType +import kotlinx.serialization.serializer +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.suggestsList +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.util.ClipboardUtils +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil +import moe.nea.firmament.util.data.MultiFileDataHolder +import moe.nea.firmament.util.tr + +object FirmWaypointManager { + object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints") + + val SHARE_PREFIX = "FIRM_WAYPOINTS/" + val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX) + + fun createExportableCopy( + waypoints: FirmWaypoints, + ): FirmWaypoints { + val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList()) + if (waypoints.isRelativeTo != null) { + val origin = waypoints.lastRelativeImport + if (origin != null) { + copy.waypoints.replaceAll { + it.copy( + x = it.x - origin.x, + y = it.y - origin.y, + z = it.z - origin.z, + ) + } + } else { + TODO("Add warning!") + } + } + return copy + } + + fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) { + val copy = waypoints.deepCopy() + if (copy.isRelativeTo != null) { + val origin = MC.player!!.blockPos + copy.waypoints.replaceAll { + it.copy( + x = it.x + origin.x, + y = it.y + origin.y, + z = it.z + origin.z, + ) + } + copy.lastRelativeImport = origin.toImmutable() + sendFeedback(tr("firmament.command.waypoint.import.ordered.success", + "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}.")) + } else { + sendFeedback(tr("firmament.command.waypoint.import.success", + "Imported ${copy.size} waypoints.")) + } + Waypoints.waypoints = copy + } + + fun setOrigin(source: DefaultSource, text: String?) { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: "" + val pos = MC.player!!.blockPos + waypoints.lastRelativeImport = pos + source.sendFeedback(tr("firmament.command.waypoint.originset", + "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position.")) + } + + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("setorigin") { + thenExecute { + setOrigin(source, null) + } + thenArgument("hint", RestArgumentType) { text -> + thenExecute { + setOrigin(source, this[text]) + } + } + } + thenLiteral("clearorigin") { + thenExecute { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.lastRelativeImport = null + waypoints.isRelativeTo = null + source.sendFeedback(tr("firmament.command.waypoint.originunset", + "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates.")) + } + } + thenLiteral("save") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + waypoints.id = get(name) + val exportableWaypoints = createExportableCopy(waypoints) + DataHolder.insert(get(name), exportableWaypoints) + DataHolder.save() + source.sendFeedback(tr("firmament.command.waypoint.saved", + "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again.")) + } + } + } + thenLiteral("load") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val name = get(name) + val waypoints = DataHolder.list()[name] + if (waypoints == null) { + source.sendError( + tr("firmament.command.waypoint.nosaved", + "No saved waypoint for ${name}. Use tab completion to see available names.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + thenLiteral("export") { + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + val exportableWaypoints = createExportableCopy(waypoints) + val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(tr("firmament.command.waypoint.export", + "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format.")) + } + } + thenLiteral("import") { + thenExecute { + val text = ClipboardUtils.getTextContents() + if (text.startsWith("[")) { + source.sendError(tr("firmament.command.waypoint.import.lookslikecw", + "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw.")) + return@thenExecute + } + val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text) + if (waypoints == null) { + source.sendError(tr("firmament.command.waypoint.import.error", + "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt new file mode 100644 index 0000000..d0cd55a --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypoints.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.util.math.BlockPos + +@Serializable +data class FirmWaypoints( + var label: String, + var id: String, + /** + * A hint to indicate where to stand while loading the waypoints. + */ + var isRelativeTo: String?, + var waypoints: MutableList<Waypoint>, + var isOrdered: Boolean, + // TODO: val resetOnSwap: Boolean, +) { + + fun deepCopy() = copy(waypoints = waypoints.toMutableList()) + @Transient + var lastRelativeImport: BlockPos? = null + + val size get() = waypoints.size + @Serializable + data class Waypoint( + val x: Int, + val y: Int, + val z: Int, + ) { + val blockPos get() = BlockPos(x, y, z) + + companion object { + fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z) + } + } +} diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt new file mode 100644 index 0000000..b36c49d --- /dev/null +++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt @@ -0,0 +1,73 @@ +package moe.nea.firmament.features.world + +import kotlin.compareTo +import kotlin.text.clear +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.world.Waypoints.TConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.render.RenderInWorldContext + +object TemporaryWaypoints { + data class TemporaryWaypoint( + val pos: BlockPos, + val postedAt: TimeMark, + ) + val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() + val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() + @Subscribe + fun onProcessChat(it: ProcessChatEvent) { + val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) + if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { + temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos( + matcher.group(1).toInt(), + matcher.group(2).toInt(), + matcher.group(3).toInt(), + ), TimeMark.now()) + } + } + @Subscribe + fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { + temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } + if (temporaryPlayerWaypointList.isEmpty()) return + RenderInWorldContext.renderInWorld(event) { + temporaryPlayerWaypointList.forEach { (_, waypoint) -> + block(waypoint.pos, 0xFFFFFF00.toInt()) + } + temporaryPlayerWaypointList.forEach { (player, waypoint) -> + val skin = + MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture + withFacingThePlayer(waypoint.pos.toCenterPos()) { + waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) + if (skin != null) { + matrixStack.translate(0F, -20F, 0F) + // Head front + texture( + skin, 16, 16, + 1 / 8f, 1 / 8f, + 2 / 8f, 2 / 8f, + ) + // Head overlay + texture( + skin, 16, 16, + 5 / 8f, 1 / 8f, + 6 / 8f, 2 / 8f, + ) + } + } + } + } + } + + @Subscribe + fun onWorldReady(event: WorldReadyEvent) { + temporaryPlayerWaypointList.clear() + } + +} diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt index 3ebfe70..b5c2b66 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -2,36 +2,24 @@ package moe.nea.firmament.features.world import com.mojang.brigadier.arguments.IntegerArgumentType import me.shedaniel.math.Color -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import net.minecraft.command.argument.BlockPosArgumentType -import net.minecraft.server.command.CommandOutput -import net.minecraft.server.command.ServerCommandSource import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d -import moe.nea.firmament.Firmament 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.ProcessChatEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldRenderLastEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig -import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC -import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.mc.asFakeServer import moe.nea.firmament.util.render.RenderInWorldContext import moe.nea.firmament.util.tr @@ -43,99 +31,85 @@ object Waypoints : FirmamentFeature { val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds } val showIndex by toggle("show-index") { true } val skipToNearest by toggle("skip-to-nearest") { false } + val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true } // TODO: look ahead size } - data class TemporaryWaypoint( - val pos: BlockPos, - val postedAt: TimeMark, - ) - override val config get() = TConfig - - val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>() - val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() - - val waypoints = mutableListOf<BlockPos>() - var ordered = false + var waypoints: FirmWaypoints? = null var orderedIndex = 0 - @Serializable - data class ColeWeightWaypoint( - val x: Int, - val y: Int, - val z: Int, - val r: Int = 0, - val g: Int = 0, - val b: Int = 0, - ) - @Subscribe fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) { - if (waypoints.isEmpty()) return + val w = useNonEmptyWaypoints() ?: return RenderInWorldContext.renderInWorld(event) { - if (!ordered) { - waypoints.withIndex().forEach { - block(it.value, 0x800050A0.toInt()) - if (TConfig.showIndex) - withFacingThePlayer(it.value.toCenterPos()) { - text(Text.literal(it.index.toString())) - } + if (!w.isOrdered) { + w.waypoints.withIndex().forEach { + block(it.value.blockPos, 0x800050A0.toInt()) + if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) { + text(Text.literal(it.index.toString())) + } } } else { - orderedIndex %= waypoints.size + orderedIndex %= w.waypoints.size val firstColor = Color.ofRGBA(0, 200, 40, 180) color(firstColor) - tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f) - waypoints.withIndex().toList() - .wrappingWindow(orderedIndex, 3) - .zip( - listOf( - firstColor, - Color.ofRGBA(180, 200, 40, 150), - Color.ofRGBA(180, 80, 20, 140), - ) - ) - .reversed() - .forEach { (waypoint, col) -> - val (index, pos) = waypoint - block(pos, col.color) - if (TConfig.showIndex) - withFacingThePlayer(pos.toCenterPos()) { - text(Text.literal(index.toString())) - } + tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), lineWidth = 3f) + w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(listOf( + firstColor, + Color.ofRGBA(180, 200, 40, 150), + Color.ofRGBA(180, 80, 20, 140), + )).reversed().forEach { (waypoint, col) -> + val (index, pos) = waypoint + block(pos.blockPos, col.color) + if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) { + text(Text.literal(index.toString())) } + } } } } @Subscribe fun onTick(event: TickEvent) { - if (waypoints.isEmpty() || !ordered) return - orderedIndex %= waypoints.size + val w = useNonEmptyWaypoints() ?: return + if (!w.isOrdered) return + orderedIndex %= w.waypoints.size val p = MC.player?.pos ?: return if (TConfig.skipToNearest) { orderedIndex = - (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size + (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size + } else { - if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) { - orderedIndex = (orderedIndex + 1) % waypoints.size + if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) { + orderedIndex = (orderedIndex + 1) % w.waypoints.size } } } + + fun useEditableWaypoints(): FirmWaypoints { + var w = waypoints + if (w == null) { + w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false) + waypoints = w + } + return w + } + + fun useNonEmptyWaypoints(): FirmWaypoints? { + val w = waypoints + if (w == null) return null + if (w.waypoints.isEmpty()) return null + return w + } + + val WAYPOINTS_SUBCOMMAND = "waypoints" + @Subscribe - fun onProcessChat(it: ProcessChatEvent) { - val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) - if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { - temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint( - BlockPos( - matcher.group(1).toInt(), - matcher.group(2).toInt(), - matcher.group(3).toInt(), - ), - TimeMark.now() - ) + fun onWorldSwap(event: WorldReadyEvent) { + if (TConfig.resetWaypointOrderOnWorldSwap) { + orderedIndex = 0 } } @@ -144,41 +118,77 @@ object Waypoints : FirmamentFeature { event.subcommand("waypoint") { thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> thenExecute { + source val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) - waypoints.add(position) - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.added", - position.x, - position.y, - position.z - ) - ) + val w = useEditableWaypoints() + w.waypoints.add(FirmWaypoints.Waypoint.from(position)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added", + position.x, + position.y, + position.z)) } } } - event.subcommand("waypoints") { + event.subcommand(WAYPOINTS_SUBCOMMAND) { + thenLiteral("reset") { + thenExecute { + orderedIndex = 0 + source.sendFeedback(tr( + "firmament.command.waypoint.reset", + "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead.")) + } + } + thenLiteral("changeindex") { + thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex -> + thenArgument("to", IntegerArgumentType.integer(0)) { toIndex -> + thenExecute { + val w = useEditableWaypoints() + val toIndex = toIndex.get(this) + val fromIndex = fromIndex.get(this) + if (fromIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(fromIndex)) + return@thenExecute + } + if (toIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(toIndex)) + return@thenExecute + } + val waypoint = w.waypoints.removeAt(fromIndex) + w.waypoints.add( + if (toIndex > fromIndex) toIndex - 1 + else toIndex, + waypoint) + source.sendFeedback( + tr("firmament.command.waypoint.indexchange", + "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints.") + ) + } + } + } + } thenLiteral("clear") { thenExecute { - waypoints.clear() + waypoints = null source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) } } thenLiteral("toggleordered") { thenExecute { - ordered = !ordered - if (ordered) { + val w = useEditableWaypoints() + w.isOrdered = !w.isOrdered + if (w.isOrdered) { val p = MC.player?.pos ?: Vec3d.ZERO - orderedIndex = - waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0 + orderedIndex = // TODO: this should be extracted to a utility method + w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0 } - source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered")) + source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}")) } } thenLiteral("skip") { thenExecute { - if (ordered && waypoints.isNotEmpty()) { - orderedIndex = (orderedIndex + 1) % waypoints.size + val w = useNonEmptyWaypoints() + if (w != null && w.isOrdered) { + orderedIndex = (orderedIndex + 1) % w.size source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) } else { source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) @@ -189,90 +199,27 @@ object Waypoints : FirmamentFeature { thenArgument("index", IntegerArgumentType.integer(0)) { indexArg -> thenExecute { val index = get(indexArg) - if (index in waypoints.indices) { - waypoints.removeAt(index) - source.sendFeedback(Text.stringifiedTranslatable( - "firmament.command.waypoint.remove", - index)) + val w = useNonEmptyWaypoints() + if (w != null && index in w.waypoints.indices) { + w.waypoints.removeAt(index) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.remove", + index)) } else { source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) } } } } - thenLiteral("export") { - thenExecute { - val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map { - ColeWeightWaypoint(it.x, - it.y, - it.z) - }) - ClipboardUtils.setTextContent(data) - source.sendFeedback(tr("firmament.command.waypoint.export", "Copied ${waypoints.size} waypoints to clipboard")) - } - } - thenLiteral("import") { - thenExecute { - val contents = ClipboardUtils.getTextContents() - val data = try { - Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(contents) - } catch (ex: Exception) { - Firmament.logger.error("Could not load waypoints from clipboard", ex) - source.sendError(Text.translatable("firmament.command.waypoint.import.error")) - return@thenExecute - } - waypoints.clear() - data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) } - source.sendFeedback( - Text.stringifiedTranslatable( - "firmament.command.waypoint.import", - data.size - ) - ) - } - } } } - @Subscribe - fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { - temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } - if (temporaryPlayerWaypointList.isEmpty()) return - RenderInWorldContext.renderInWorld(event) { - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - block(waypoint.pos, 0xFFFFFF00.toInt()) - } - temporaryPlayerWaypointList.forEach { (player, waypoint) -> - val skin = - MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player } - ?.skinTextures - ?.texture - withFacingThePlayer(waypoint.pos.toCenterPos()) { - waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) - if (skin != null) { - matrixStack.translate(0F, -20F, 0F) - // Head front - texture( - skin, 16, 16, - 1 / 8f, 1 / 8f, - 2 / 8f, 2 / 8f, - ) - // Head overlay - texture( - skin, 16, 16, - 5 / 8f, 1 / 8f, - 6 / 8f, 2 / 8f, - ) - } - } - } - } - } + fun textInvalidIndex(index: Int) = + tr("firmament.command.waypoint.invalid-index", + "Invalid index $index provided.") - @Subscribe - fun onWorldReady(event: WorldReadyEvent) { - temporaryPlayerWaypointList.clear() - } + fun textNothingToExport(): Text = + tr("firmament.command.waypoint.export.nowaypoints", + "No waypoints to export found. Add some with /firm waypoint ~ ~ ~.") } fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { @@ -285,35 +232,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> { } return result } - - -fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { - val source = this - return ServerCommandSource( - object : CommandOutput { - override fun sendMessage(message: Text?) { - source.player.sendMessage(message, false) - } - - override fun shouldReceiveFeedback(): Boolean { - return true - } - - override fun shouldTrackOutput(): Boolean { - return true - } - - override fun shouldBroadcastConsoleToOps(): Boolean { - return true - } - }, - source.position, - source.rotation, - null, - 0, - "FakeServerCommandSource", - Text.literal("FakeServerCommandSource"), - null, - source.player - ) -} |