diff options
Diffstat (limited to 'src/main/kotlin/features')
23 files changed, 1580 insertions, 479 deletions
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 0f5ebf8..f0c1857 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -31,93 +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 - init { - autoload() - } + 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/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt new file mode 100644 index 0000000..de3a0d9 --- /dev/null +++ b/src/main/kotlin/features/chat/PartyCommands.kt @@ -0,0 +1,134 @@ +package moe.nea.firmament.features.chat + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.tree.LiteralCommandNode +import kotlin.time.Duration.Companion.seconds +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.PartyMessageReceivedEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.useMatch + +object PartyCommands { + + val messageInChannel = "(?<channel>Party|Guild) >([^:]+?)? (?<name>[^: ]+): (?<message>.+)".toPattern() + + @Subscribe + fun onChat(event: ProcessChatEvent) { + messageInChannel.useMatch(event.unformattedString) { + val channel = group("channel") + val message = group("message") + val name = group("name") + if (channel == "Party") { + PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent( + event, message, name + )) + } + } + } + + val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet() + + data class PartyCommandContext( + val name: String + ) + + val dispatch = CommandDispatcher<PartyCommandContext>().also { dispatch -> + fun register( + name: String, + vararg alias: String, + block: CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>.() -> Unit = {}, + ): LiteralCommandNode<PartyCommandContext> { + val node = + dispatch.register(CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>(name).also(block)) + alias.forEach { register(it) { redirect(node) } } + return node + } + + register("warp", "pw", "pwarp", "partywarp") { + executes { + // TODO: add check if you are the party leader + MC.sendCommand("p warp") + 0 + } + } + + register("transfer", "pt", "ptme") { + executes { + MC.sendCommand("p transfer ${it.source.name}") + 0 + } + } + + register("allinvite", "allinv") { + executes { + MC.sendCommand("p settings allinvite") + 0 + } + } + + register("coords") { + executes { + val p = MC.player?.blockPos ?: BlockPos.ORIGIN + MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}") + 0 + } + } + // TODO: downtime tracker (display message again at end of dungeon) + // instance ends: kuudra, dungeons, bacte + // TODO: at TPS command + } + + object TConfig : ManagedConfig("party-commands", Category.CHAT) { + val enable by toggle("enable") { false } + val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds } + val ignoreOwnCommands by toggle("ignore-own") { false } + } + + var lastCommand = TimeMark.farPast() + + @Subscribe + fun listPartyCommands(event: CommandEvent.SubCommand) { + event.subcommand("partycommands") { + thenExecute { + // TODO: Better help, including descriptions and redirect detection + MC.sendChat(tr("firmament.partycommands.help", "Available party commands: ${dispatch.root.children.map { it.name }}. Available prefixes: $commandPrefixes")) + } + } + } + + @Subscribe + fun onPartyMessage(event: PartyMessageReceivedEvent) { + if (!TConfig.enable) return + if (event.message.firstOrNull() !in commandPrefixes) return + if (event.name == MC.playerName && TConfig.ignoreOwnCommands) return + if (lastCommand.passedTime() < TConfig.cooldown) { + MC.sendChat(tr("firmament.partycommands.cooldown", "Skipping party command. Cooldown not passed.")) + return + } + // TODO: add trust levels + val commandLine = event.message.substring(1) + try { + dispatch.execute(StringReader(commandLine), PartyCommandContext(event.name)) + } catch (ex: Exception) { + if (ex is CommandSyntaxException) { + MC.sendChat(tr("firmament.partycommands.unknowncommand", "Unknown party command.")) + return + } else { + MC.sendChat(tr("firmament.partycommands.unknownerror", "Unknown error during command execution.")) + ErrorUtil.softError("Unknown error during command execution.", ex) + } + } + lastCommand = TimeMark.now() + } +} diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt index 5944b92..7963171 100644 --- a/src/main/kotlin/features/chat/QuickCommands.kt +++ b/src/main/kotlin/features/chat/QuickCommands.kt @@ -1,8 +1,12 @@ - - package moe.nea.firmament.features.chat +import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.context.CommandContext +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.fabricmc.fabric.impl.command.client.ClientCommandInternals +import net.minecraft.command.CommandRegistryAccess +import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket import net.minecraft.text.Text import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.DefaultSource @@ -12,89 +16,139 @@ import moe.nea.firmament.commands.thenArgument import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.events.CommandEvent import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.config.ManagedOption import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.tr object QuickCommands : FirmamentFeature { - override val identifier: String - get() = "quick-commands" + override val identifier: String + get() = "quick-commands" + + object TConfig : ManagedConfig("quick-commands", Category.CHAT) { + val enableJoin by toggle("join") { true } + val enableDh by toggle("dh") { true } + override fun onChange(option: ManagedOption<*>) { + reloadCommands() + } + } + + fun reloadCommands() { + val lastPacket = lastReceivedTreePacket ?: return + val network = MC.networkHandler ?: return + val fallback = ClientCommandInternals.getActiveDispatcher() + try { + val dispatcher = CommandDispatcher<FabricClientCommandSource>() + ClientCommandInternals.setActiveDispatcher(dispatcher) + ClientCommandRegistrationCallback.EVENT.invoker() + .register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries, + network.enabledFeatures)) + ClientCommandInternals.finalizeInit() + network.onCommandTree(lastPacket) + } catch (ex: Exception) { + ClientCommandInternals.setActiveDispatcher(fallback) + throw ex + } + } + + + fun removePartialPrefix(text: String, prefix: String): String? { + var lf: String? = null + for (i in 1..prefix.length) { + if (text.startsWith(prefix.substring(0, i))) { + lf = text.substring(i) + } + } + return lf + } + + var lastReceivedTreePacket: CommandTreeS2CPacket? = null - fun removePartialPrefix(text: String, prefix: String): String? { - var lf: String? = null - for (i in 1..prefix.length) { - if (text.startsWith(prefix.substring(0, i))) { - lf = text.substring(i) - } - } - return lf - } + val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") + val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") - val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") - val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") + @Subscribe + fun registerDh(event: CommandEvent) { + if (!TConfig.enableDh) return + event.register("dh") { + thenExecute { + MC.sendCommand("warp dhub") + } + } + event.register("dn") { + thenExecute { + MC.sendChat(tr("firmament.quickwarp.deez-nutz", "Warping to... Deez Nuts!").grey()) + MC.sendCommand("warp dhub") + } + } + } - @Subscribe - fun onCommands(it: CommandEvent) { - it.register("join") { - thenArgument("what", RestArgumentType) { what -> - thenExecute { - val what = this[what] - if (!SBData.isOnSkyblock) { - MC.sendCommand("join $what") - return@thenExecute - } - val joinName = getNameForFloor(what.replace(" ", "").lowercase()) - if (joinName == null) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what)) - } else { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success", - joinName)) - MC.sendCommand("joininstance $joinName") - } - } - } - thenExecute { - source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain")) - } - } - } + @Subscribe + fun registerJoin(it: CommandEvent) { + if (!TConfig.enableJoin) return + it.register("join") { + thenArgument("what", RestArgumentType) { what -> + thenExecute { + val what = this[what] + if (!SBData.isOnSkyblock) { + MC.sendCommand("join $what") + return@thenExecute + } + val joinName = getNameForFloor(what.replace(" ", "").lowercase()) + if (joinName == null) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what)) + } else { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success", + joinName)) + MC.sendCommand("joininstance $joinName") + } + } + } + thenExecute { + source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain")) + } + } + } - fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? { - val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier") - if (kuudraLevel != null) { - val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst { - it.startsWith( - kuudraLevel, - true - ) - } - if (l !in kuudraLevelNames.indices) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra", - kuudraLevel)) - return null - } - return "KUUDRA_${kuudraLevelNames[l]}" - } - val masterLevel = removePartialPrefix(w, "master") - val normalLevel = - removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons") - val dungeonLevel = masterLevel ?: normalLevel - if (dungeonLevel != null) { - val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst { - it.startsWith( - dungeonLevel, - true - ) - } - if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) { - return "CATACOMBS_ENTRANCE" - } - if (l !in dungeonLevelNames.indices) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs", - kuudraLevel)) - return null - } - return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}" - } - return null - } + fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? { + val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier") + if (kuudraLevel != null) { + val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst { + it.startsWith( + kuudraLevel, + true + ) + } + if (l !in kuudraLevelNames.indices) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra", + kuudraLevel)) + return null + } + return "KUUDRA_${kuudraLevelNames[l]}" + } + val masterLevel = removePartialPrefix(w, "master") + val normalLevel = + removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons") + val dungeonLevel = masterLevel ?: normalLevel + if (dungeonLevel != null) { + val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst { + it.startsWith( + dungeonLevel, + true + ) + } + if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) { + return "CATACOMBS_ENTRANCE" + } + if (l !in dungeonLevelNames.indices) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs", + kuudraLevel)) + return null + } + return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}" + } + return null + } } diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..11b47a9 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,51 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.component.DataComponentTypes +import net.minecraft.entity.Entity +import moe.nea.firmament.annotations.Subscribe +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.util.MC +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr + +object AnimatedClothingScanner { + + var observedEntity: Entity? = null + + @OptIn(ExperimentalStdlibApi::class) + @Subscribe + fun onUpdate(event: EntityUpdateEvent) { + if (event.entity != observedEntity) return + if (event is EntityUpdateEvent.EquipmentUpdate) { + event.newEquipment.forEach { + val id = it.second.skyBlockId?.neuItem + val colour = it.second.get(DataComponentTypes.DYED_COLOR) + ?.rgb?.toHexString(HexFormat.UpperCase) + ?.let { " #$it" } ?: "" + MC.sendChat(tr("firmament.fitstealer.update", + "[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => ${id}${colour}")) + } + } + } + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand("dev") { + thenLiteral("stealthisfit") { + thenExecute { + observedEntity = + if (observedEntity == null) MC.instance.targetedEntity else null + + MC.sendChat( + observedEntity?.let { + tr("firmament.fitstealer.targeted", "Observing the equipment of ${it.name}.") + } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), + ) + } + } + } + } +} 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/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 225bc13..8be5d5d 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -1,22 +1,31 @@ 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.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.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 @@ -30,6 +39,8 @@ import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.focusedItemStack +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.loreAccordingToNbt @@ -87,6 +98,11 @@ object PowerUserTools : FirmamentFeature { } fun showEntity(target: Entity) { + val nbt = NbtPredicate.entityToNbt(target) + nbt.remove("Inventory") + nbt.put("StyledName", TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, target.styledDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type)) MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name)) MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) @@ -119,7 +135,11 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) } else if (it.matches(TConfig.copyTexturePackId)) { - val model = CustomItemModelEvent.getModelIdentifier(item) // TODO: remove global texture overrides, maybe + val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager { + override fun hasModel_firmament(identifier: Identifier): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe if (model == null) { lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) return diff --git a/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt new file mode 100644 index 0000000..9935051 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.events.anniversity + +import java.util.Optional +import me.shedaniel.math.Color +import kotlin.jvm.optionals.getOrNull +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Style +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object CenturyRaffleFeatures { + object TConfig : ManagedConfig("centuryraffle", Category.EVENTS) { + val highlightPlayersForSlice by toggle("highlight-cake-players") { true } +// val highlightAllPlayers by toggle("highlight-all-cake-players") { true } + } + + val cakeIcon = "⛃" + + val cakeColors = listOf( + CakeTeam(SkyBlockItems.SLICE_OF_BLUEBERRY_CAKE, Formatting.BLUE), + CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, Formatting.YELLOW), + CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, Formatting.GREEN), + CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, Formatting.RED), + CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, Formatting.LIGHT_PURPLE), + ) + + data class CakeTeam( + val id: SkyblockId, + val formatting: Formatting, + ) { + val searchedTextRgb = formatting.colorValue!! + val brightenedRgb = Color.ofOpaque(searchedTextRgb)//.brighter(2.0) + val tintOverlay by lazy { + TintedOverlayTexture().setColor(brightenedRgb) + } + } + + val sliceToColor = cakeColors.associateBy { it.id } + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightPlayersForSlice) return + val requestedCakeTeam = sliceToColor[MC.stackInHand?.skyBlockId] ?: return + // TODO: cache the requested color + val player = event.entity as? PlayerEntity ?: return + val cakeColor: Style = player.styledDisplayName.visit( + { style, text -> + if (text == cakeIcon) Optional.of(style) + else Optional.empty() + }, Style.EMPTY).getOrNull() ?: return + if (cakeColor.color?.rgb == requestedCakeTeam.searchedTextRgb) { + event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay + } + } + +} diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt index 5d70b1a..3dae233 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -1,71 +1,68 @@ - - package moe.nea.firmament.features.fixes import moe.nea.jarvis.api.Point import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable import net.minecraft.client.MinecraftClient import net.minecraft.client.option.KeyBinding -import net.minecraft.entity.player.PlayerEntity import net.minecraft.text.Text -import net.minecraft.util.Arm import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.WorldKeyboardEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.MC -import moe.nea.firmament.util.errorBoundary object Fixes : FirmamentFeature { - override val identifier: String - get() = "fixes" + override val identifier: String + get() = "fixes" - object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config - val fixUnsignedPlayerSkins by toggle("player-skins") { true } - var autoSprint by toggle("auto-sprint") { false } - val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding") - val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) } - val peekChat by keyBindingWithDefaultUnbound("peek-chat") - } + object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config + val fixUnsignedPlayerSkins by toggle("player-skins") { true } + var autoSprint by toggle("auto-sprint") { false } + val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding") + val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) } + val peekChat by keyBindingWithDefaultUnbound("peek-chat") + val hidePotionEffects by toggle("hide-mob-effects") { false } + val noHurtCam by toggle("disable-hurt-cam") { false } + } - override val config: ManagedConfig - get() = TConfig + override val config: ManagedConfig + get() = TConfig - fun handleIsPressed( - keyBinding: KeyBinding, - cir: CallbackInfoReturnable<Boolean> - ) { - if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true) - cir.returnValue = true - } + fun handleIsPressed( + keyBinding: KeyBinding, + cir: CallbackInfoReturnable<Boolean> + ) { + if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true) + cir.returnValue = true + } - @Subscribe - fun onRenderHud(it: HudRenderEvent) { - if (!TConfig.autoSprintKeyBinding.isBound) return - it.context.matrices.push() - TConfig.autoSprintHud.applyTransformations(it.context.matrices) - it.context.drawText( - MC.font, Text.translatable( - if (TConfig.autoSprint) - "firmament.fixes.auto-sprint.on" - else if (MC.player?.isSprinting == true) - "firmament.fixes.auto-sprint.sprinting" - else - "firmament.fixes.auto-sprint.not-sprinting" - ), 0, 0, -1, false - ) - it.context.matrices.pop() - } + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.autoSprintKeyBinding.isBound) return + it.context.matrices.push() + TConfig.autoSprintHud.applyTransformations(it.context.matrices) + it.context.drawText( + MC.font, Text.translatable( + if (TConfig.autoSprint) + "firmament.fixes.auto-sprint.on" + else if (MC.player?.isSprinting == true) + "firmament.fixes.auto-sprint.sprinting" + else + "firmament.fixes.auto-sprint.not-sprinting" + ), 0, 0, -1, false + ) + it.context.matrices.pop() + } - @Subscribe - fun onWorldKeyboard(it: WorldKeyboardEvent) { - if (it.matches(TConfig.autoSprintKeyBinding)) { - TConfig.autoSprint = !TConfig.autoSprint - } - } + @Subscribe + fun onWorldKeyboard(it: WorldKeyboardEvent) { + if (it.matches(TConfig.autoSprintKeyBinding)) { + TConfig.autoSprint = !TConfig.autoSprint + } + } - fun shouldPeekChat(): Boolean { - return TConfig.peekChat.isPressed(atLeast = true) - } + fun shouldPeekChat(): Boolean { + return TConfig.peekChat.isPressed(atLeast = true) + } } diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt index d2c555b..fdc378a 100644 --- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -29,18 +29,7 @@ object ItemRarityCosmetics : FirmamentFeature { override val config: ManagedConfig get() = TConfig - private val rarityToColor = mapOf( - Rarity.COMMON to Formatting.WHITE, - Rarity.UNCOMMON to Formatting.GREEN, - Rarity.RARE to Formatting.BLUE, - Rarity.EPIC to Formatting.DARK_PURPLE, - Rarity.LEGENDARY to Formatting.GOLD, - Rarity.MYTHIC to Formatting.LIGHT_PURPLE, - Rarity.DIVINE to Formatting.AQUA, - Rarity.SPECIAL to Formatting.RED, - Rarity.VERY_SPECIAL to Formatting.RED, - Rarity.SUPREME to Formatting.DARK_RED, - ).mapValues { + private val rarityToColor = Rarity.colourMap.mapValues { val c = Color(it.value.colorValue!!) c.rgb } diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index fc09476..0083c40 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -2,11 +2,19 @@ package moe.nea.firmament.features.inventory -import com.mojang.blaze3d.systems.RenderSystem import java.util.UUID import org.lwjgl.glfw.GLFW +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.entity.player.PlayerInventory @@ -14,7 +22,9 @@ import net.minecraft.screen.GenericContainerScreenHandler import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FeaturesInitializedEvent import moe.nea.firmament.events.HandledScreenForegroundEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyReleasedEvent @@ -37,6 +47,7 @@ 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 import moe.nea.firmament.util.unformattedString @@ -49,9 +60,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") { @@ -59,6 +127,19 @@ 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 } + } + + enum class SlotRenderLinesMode : StringIdentifiable { + EVERYTHING, + ONLY_BOXES, + NOTHING; + + override fun asString(): String { + return name + } } override val config: TConfig @@ -95,7 +176,7 @@ object SlotLocking : FirmamentFeature { if (handler.inventory.size() < 9) return false val sellItem = handler.inventory.getStack(handler.inventory.size() - 5) if (sellItem == null) return false - if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true + if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true val lore = sellItem.loreAccordingToNbt return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!" } @@ -104,12 +185,16 @@ object SlotLocking : FirmamentFeature { fun onSalvageProtect(event: IsSlotProtectedEvent) { if (event.slot == null) return if (!event.slot.hasStack()) return - if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return + if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return val inv = event.slot.inventory var anyBlocked = false for (i in 0 until event.slot.index) { val stack = inv.getStack(i) - if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) + if (IsSlotProtectedEvent.shouldBlockInteraction(null, + SlotActionType.THROW, + IsSlotProtectedEvent.MoveOrigin.SALVAGE, + stack) + ) anyBlocked = true } if (anyBlocked) { @@ -145,20 +230,33 @@ object SlotLocking : FirmamentFeature { } @Subscribe + fun onEvent(event: FeaturesInitializedEvent) { + IsSlotProtectedEvent.subscribe(receivesCancelled = true, "SlotLocking:unlockInDungeons") { + if (it.isProtected + && it.origin == IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR + && DungeonUtil.isInActiveDungeon + && TConfig.allowDroppingInDungeons + ) { + it.isProtected = false + } + } + } + + @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) } } @@ -197,10 +295,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 @@ -214,9 +310,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) } } @@ -227,23 +321,38 @@ object SlotLocking : FirmamentFeature { val accScreen = event.screen as AccessorHandledScreen val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament - boundSlots.entries.forEach { - val hotbarSlot = findByIndex(it.key) ?: return@forEach - val inventorySlot = findByIndex(it.value) ?: return@forEach + 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() - event.context.drawLine( - invX + sx, invY + sy, - hotX + sx, hotY + sy, - me.shedaniel.math.Color.ofOpaque(0x00FF00) - ) + val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot + || accScreen.focusedSlot_Firmament === inventorySlot + if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) + continue + 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(anyHovered) + ) event.context.drawBorder(hotbarSlot.x + sx, hotbarSlot.y + sy, - 16, 16, 0xFF00FF00u.toInt()) + 16, 16, color(hotbarSlot in highlitSlots).color) event.context.drawBorder(inventorySlot.x + sx, inventorySlot.y + sy, - 16, 16, 0xFF00FF00u.toInt()) + 16, 16, color(inventorySlot in highlitSlots).color) } } @@ -299,11 +408,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) diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt new file mode 100644 index 0000000..309ea61 --- /dev/null +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.features.inventory + +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.format.FormatStyle +import java.time.format.TextStyle +import java.time.temporal.ChronoField +import net.minecraft.text.Text +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.aqua +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString + +object TimerInLore { + object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) { + val showTimers by toggle("show") { true } + val timerFormat by choice("format") { TimerFormat.SOCIALIST } + } + + enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable { + RFC(DateTimeFormatter.RFC_1123_DATE_TIME), + LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)), + SOCIALIST( + { + appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) + appendLiteral(" ") + appendValue(ChronoField.DAY_OF_MONTH, 2) + appendLiteral(".") + appendValue(ChronoField.MONTH_OF_YEAR, 2) + appendLiteral(".") + appendValue(ChronoField.YEAR, 4) + appendLiteral(" ") + appendValue(ChronoField.HOUR_OF_DAY, 2) + appendLiteral(":") + appendValue(ChronoField.MINUTE_OF_HOUR, 2) + appendLiteral(":") + appendValue(ChronoField.SECOND_OF_MINUTE, 2) + }), + AMERICAN("EEEE, MMM d h:mm a yyyy"), + ; + + constructor(block: DateTimeFormatterBuilder.() -> Unit) + : this(DateTimeFormatterBuilder().also(block).toFormatter()) + + constructor(format: String) : this(DateTimeFormatter.ofPattern(format)) + + override fun asString(): String { + return name + } + } + + enum class CountdownTypes( + val match: String, + val label: String, // TODO: convert to a string + val isRelative: Boolean = false, + ) { + STARTING("Starting in:", "Starts at"), + STARTS("Starts in:", "Starts at"), + INTEREST("Interest in:", "Interest at"), + UNTILINTEREST("Until interest:", "Interest at"), + ENDS("Ends in:", "Ends at"), + REMAINING("Remaining:", "Ends at"), + DURATION("Duration:", "Finishes at"), + TIMELEFT("Time left:", "Ends at"), + EVENTTIMELEFT("Event lasts for", "Ends at", isRelative = true), + SHENSUCKS("Auction ends in:", "Auction ends at"), + ENDS_PET_LEVELING( + "Ends:", + "Finishes at" + ), + CALENDARDETAILS(" (§e", "Starts at"), + COMMUNITYPROJECTS("Contribute again", "Come back at"), + CHOCOLATEFACTORY("Next Charge", "Available at"), + STONKSAUCTION("Auction ends in", "Ends at"), + LIZSTONKREDEMPTION("Resets in:", "Resets at"), + EVENTENDING("Event ends in:", "Ends at"); + } + + val regex = + "(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex() + + @Subscribe + fun modifyLore(event: ItemTooltipEvent) { + if (!TConfig.showTimers) return + var lastTimer: ZonedDateTime? = null + for (i in event.lines.indices) { + val line = event.lines[i].unformattedString + val countdownType = CountdownTypes.entries.find { it.match in line } ?: continue + if (countdownType == CountdownTypes.CALENDARDETAILS + && !event.stack.displayNameAccordingToNbt.unformattedString.startsWith("Day ") + ) continue + + val countdownMatch = regex.findAll(line).filter { it.value.isNotBlank() }.lastOrNull() ?: continue + val (years, days, hours, minutes, seconds) = + listOf("years", "days", "hours", "minutes", "seconds") + .map { + countdownMatch.groups[it]?.value?.toLong() ?: 0L + } + if (years + days + hours + minutes + seconds == 0L) continue + var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone) + if (countdownType.isRelative) { + if (lastTimer == null) { + event.lines.add(i + 1, + tr("firmament.loretimer.missingrelative", + "Found a relative countdown with no baseline (Firmament)").grey()) + continue + } + baseLine = lastTimer + } + val timer = + baseLine.plusYears(years).plusDays(days).plusHours(hours).plusMinutes(minutes).plusSeconds(seconds) + lastTimer = timer + val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault()) + // TODO: install approximate time stabilization algorithm + event.lines.add(i + 1, + Text.literal("${countdownType.label}: ") + .grey() + .append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua()) + ) + } + } + +} 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..92640c8 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -78,9 +78,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/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt index cf1cf1d..63a2f54 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -35,6 +35,7 @@ import moe.nea.firmament.util.mc.FakeSlot import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.render.enableScissorWithoutTranslation import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString @@ -241,9 +242,9 @@ class StorageOverlayScreen : Screen(Text.literal("")) { fun createScissors(context: DrawContext) { val rect = getScrollPanelInner() - context.enableScissor( - rect.minX, rect.minY, - rect.maxX, rect.maxY + context.enableScissorWithoutTranslation( + rect.minX.toFloat(), rect.minY.toFloat(), + rect.maxX.toFloat(), rect.maxY.toFloat(), ) } @@ -302,6 +303,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + guiContext.setFocusedElement(null) // Blur all elements. They will be refocused by clickMCComponentInPlace if in doubt, and we don't have any double click components. if (getScrollPanelInner().contains(mouseX, mouseY)) { val data = StorageOverlay.Data.data ?: StorageData() layoutedForEach(data) { rect, page, _ -> @@ -355,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 e07df8a..3b86184 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.inventory.storageoverlay import io.ktor.util.decodeBase64Bytes @@ -19,47 +17,59 @@ import net.minecraft.nbt.NbtIo import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtOps import net.minecraft.nbt.NbtSizeTracker +import net.minecraft.registry.RegistryOps +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.TolerantRegistriesOps @Serializable(with = VirtualInventory.Serializer::class) data class VirtualInventory( - val stacks: List<ItemStack> + val stacks: List<ItemStack> ) { - val rows = stacks.size / 9 + val rows = stacks.size / 9 + + init { + assert(stacks.size % 9 == 0) + assert(stacks.size / 9 in 1..5) + } - init { - assert(stacks.size % 9 == 0) - assert(stacks.size / 9 in 1..5) - } + object Serializer : KSerializer<VirtualInventory> { + const val INVENTORY = "INVENTORY" + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING) - object Serializer : KSerializer<VirtualInventory> { - const val INVENTORY = "INVENTORY" - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): VirtualInventory { + val s = decoder.decodeString() + val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000)) + val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt()) + val ops = getOps() + return VirtualInventory(items.map { + it as NbtCompound + if (it.isEmpty) ItemStack.EMPTY + else ErrorUtil.catch("Could not deserialize item") { + ItemStack.CODEC.parse(ops, it).orThrow + }.or { ItemStack.EMPTY } + }) + } - override fun deserialize(decoder: Decoder): VirtualInventory { - val s = decoder.decodeString() - val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000)) - val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt()) - return VirtualInventory(items.map { - it as NbtCompound - if (it.isEmpty) ItemStack.EMPTY - else runCatching { - ItemStack.CODEC.parse(NbtOps.INSTANCE, it).orThrow - }.getOrElse { ItemStack.EMPTY } - }) - } + fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) - override fun serialize(encoder: Encoder, value: VirtualInventory) { - val list = NbtList() - value.stacks.forEach { - if (it.isEmpty) list.add(NbtCompound()) - else list.add(runCatching { ItemStack.CODEC.encode(it, NbtOps.INSTANCE, NbtCompound()).orThrow } - .getOrElse { NbtCompound() }) - } - val baos = ByteArrayOutputStream() - NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos) - encoder.encodeString(baos.toByteArray().encodeBase64()) - } - } + override fun serialize(encoder: Encoder, value: VirtualInventory) { + val list = NbtList() + val ops = getOps() + value.stacks.forEach { + if (it.isEmpty) list.add(NbtCompound()) + else list.add(ErrorUtil.catch("Could not serialize item") { + ItemStack.CODEC.encode(it, + ops, + NbtCompound()).orThrow + } + .or { NbtCompound() }) + } + val baos = ByteArrayOutputStream() + NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos) + encoder.encodeString(baos.toByteArray().encodeBase64()) + } + } } diff --git a/src/main/kotlin/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 2d6c3ee..1737969 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -50,7 +50,6 @@ object PickaxeAbility : FirmamentFeature { val drillFuelBar by toggle("fuel-bar") { true } val blockOnPrivateIsland by choice( "block-on-dynamic", - BlockPickaxeAbility.entries, ) { BlockPickaxeAbility.ONLY_DESTRUCTIVE } @@ -99,6 +98,7 @@ object PickaxeAbility : FirmamentFeature { @Subscribe fun onPickaxeRightClick(event: UseItemEvent) { if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return + if (SBData.skyblockLocation != SkyBlockIsland.PRIVATE_ISLAND && SBData.skyblockLocation != SkyBlockIsland.GARDEN) return val itemType = ItemType.fromItemStack(event.item) if (itemType !in pickaxeTypes) return val ability = AbilityUtils.getAbilities(event.item) @@ -156,10 +156,21 @@ object PickaxeAbility : FirmamentFeature { fun onChatMessage(it: ProcessChatEvent) { abilityUsePattern.useMatch(it.unformattedString) { lastUsage[group("name")] = TimeMark.now() + abilityOverride = group("name") } abilitySwitchPattern.useMatch(it.unformattedString) { abilityOverride = group("ability") } + pickaxeAbilityCooldownPattern.useMatch(it.unformattedString) { + val ability = abilityOverride ?: return@useMatch + val remainingCooldown = parseTimePattern(group("remainingCooldown")) + val length = defaultAbilityDurations[ability] ?: return@useMatch + lastUsage[ability] = TimeMark.ago(length - remainingCooldown) + } + nowAvailable.useMatch(it.unformattedString) { + val ability = group("name") + lastUsage[ability] = TimeMark.farPast() + } } @Subscribe @@ -179,6 +190,7 @@ object PickaxeAbility : FirmamentFeature { val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)") val pickaxeAbilityCooldownPattern = Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.") + val nowAvailable = Pattern.compile("(?<name>[a-zA-Z0-9 ]+) is now available!") data class PickaxeAbilityData( val name: String, diff --git a/src/main/kotlin/features/misc/TimerFeature.kt b/src/main/kotlin/features/misc/TimerFeature.kt new file mode 100644 index 0000000..7c4833d --- /dev/null +++ b/src/main/kotlin/features/misc/TimerFeature.kt @@ -0,0 +1,124 @@ +package moe.nea.firmament.features.misc + +import com.mojang.brigadier.arguments.IntegerArgumentType +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DurationArgumentType +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.util.CommonSoundEffects +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MinecraftDispatcher +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.clickCommand +import moe.nea.firmament.util.lime +import moe.nea.firmament.util.red +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.yellow + +object TimerFeature { + data class Timer( + val start: TimeMark, + val duration: Duration, + val message: String, + val timerId: Int, + ) { + fun timeLeft() = (duration - start.passedTime()).coerceAtLeast(0.seconds) + fun isDone() = start.passedTime() >= duration + } + + // Theoretically for optimal performance this could be a treeset keyed to the end time + val timers = mutableListOf<Timer>() + + @Subscribe + fun tick(event: TickEvent) { + timers.removeAll { + if (it.isDone()) { + MC.sendChat(tr("firmament.timer.finished", + "The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}") + .yellow()) + Firmament.coroutineScope.launch { + withContext(MinecraftDispatcher) { + repeat(5) { + CommonSoundEffects.playSuccess() + delay(0.2.seconds) + } + } + } + true + } else { + false + } + } + } + + fun startTimer(duration: Duration, message: String) { + val timerId = createTimerId++ + timers.add(Timer(TimeMark.now(), duration, message, timerId)) + MC.sendChat( + tr("firmament.timer.start", + "Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime() + .append(" ") + .append( + tr("firmament.timer.cancelbutton", + "Click here to cancel the timer." + ).clickCommand("/firm timer clear $timerId").red() + ) + ) + } + + fun clearTimer(timerId: Int) { + val timer = timers.indexOfFirst { it.timerId == timerId } + if (timer < 0) { + MC.sendChat(tr("firmament.timer.cancel.fail", + "Could not cancel that timer. Maybe it was already cancelled?").red()) + } else { + val timerData = timers[timer] + timers.removeAt(timer) + MC.sendChat(tr("firmament.timer.cancel.done", + "Cancelled timer ${timerData.message}. It would have been done in ${ + FirmFormatters.formatTimespan(timerData.timeLeft()) + }.").lime()) + } + } + + var createTimerId = 0 + + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + event.subcommand("cleartimer") { + thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId -> + thenExecute { + clearTimer(this[timerId]) + } + } + thenExecute { + timers.map { it.timerId }.forEach { + clearTimer(it) + } + } + } + event.subcommand("timer") { + thenArgument("time", DurationArgumentType) { duration -> + thenExecute { + startTimer(this[duration], "no message") + } + thenArgument("message", RestArgumentType) { message -> + thenExecute { + startTimer(this[duration], this[message]) + } + } + } + } + } +} 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 16db059..b5c2b66 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -2,36 +2,26 @@ 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 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 object Waypoints : FirmamentFeature { override val identifier: String @@ -41,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 } } @@ -142,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")) @@ -187,79 +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("import") { - thenExecute { - val contents = ClipboardUtils.getTextContents() - val data = try { - Firmament.json.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> { @@ -272,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 - ) -} |