diff options
Diffstat (limited to 'src/main/kotlin/features')
94 files changed, 7570 insertions, 1890 deletions
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt index 0f5ebf8..d474d65 100644 --- a/src/main/kotlin/features/FeatureManager.kt +++ b/src/main/kotlin/features/FeatureManager.kt @@ -1,123 +1,25 @@ package moe.nea.firmament.features -import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer -import moe.nea.firmament.Firmament -import moe.nea.firmament.events.FeaturesInitializedEvent import moe.nea.firmament.events.FirmamentEvent import moe.nea.firmament.events.subscription.Subscription import moe.nea.firmament.events.subscription.SubscriptionList -import moe.nea.firmament.features.chat.AutoCompletions -import moe.nea.firmament.features.chat.ChatLinks -import moe.nea.firmament.features.chat.QuickCommands -import moe.nea.firmament.features.debug.DebugView -import moe.nea.firmament.features.debug.DeveloperFeatures -import moe.nea.firmament.features.debug.MinorTrolling -import moe.nea.firmament.features.debug.PowerUserTools -import moe.nea.firmament.features.diana.DianaWaypoints -import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures -import moe.nea.firmament.features.events.carnival.CarnivalFeatures -import moe.nea.firmament.features.fixes.CompatibliltyFeatures -import moe.nea.firmament.features.fixes.Fixes -import moe.nea.firmament.features.inventory.CraftingOverlay -import moe.nea.firmament.features.inventory.ItemRarityCosmetics -import moe.nea.firmament.features.inventory.PetFeatures -import moe.nea.firmament.features.inventory.PriceData -import moe.nea.firmament.features.inventory.SaveCursorPosition -import moe.nea.firmament.features.inventory.SlotLocking -import moe.nea.firmament.features.inventory.buttons.InventoryButtons -import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay -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.data.DataHolder - -object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) { - @Serializable - data class Config( - val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() - ) - - private val features = mutableMapOf<String, FirmamentFeature>() - - val allFeatures: Collection<FirmamentFeature> get() = features.values - - 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 subscribeEvents() { - SubscriptionList.allLists.forEach { - it.provideSubscriptions { - it.owner.javaClass.classes.forEach { - runCatching { it.getDeclaredField("INSTANCE").get(null) } +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.compatloader.ICompatMeta + +object FeatureManager { + + fun subscribeEvents() { + SubscriptionList.allLists.forEach { list -> + if (ICompatMeta.shouldLoad(list.javaClass.name)) + ErrorUtil.catch("Error while loading events from $list") { + list.provideSubscriptions { + subscribeSingleEvent(it) + } } - subscribeSingleEvent(it) - } - } - } - - 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 isEnabled(identifier: String): Boolean? = - data.enabledFeatures[identifier] - - - fun setEnabled(identifier: String, value: Boolean) { - data.enabledFeatures[identifier] = value - markDirty() - } + } + } + private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) { + it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) + } } diff --git a/src/main/kotlin/features/FirmamentFeature.kt b/src/main/kotlin/features/FirmamentFeature.kt deleted file mode 100644 index 2cfc4fd..0000000 --- a/src/main/kotlin/features/FirmamentFeature.kt +++ /dev/null @@ -1,23 +0,0 @@ - - -package moe.nea.firmament.features - -import moe.nea.firmament.events.subscription.SubscriptionOwner -import moe.nea.firmament.gui.config.ManagedConfig - -// TODO: remove this entire feature system and revamp config -interface FirmamentFeature : SubscriptionOwner { - val identifier: String - val defaultEnabled: Boolean - get() = true - var isEnabled: Boolean - get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled - set(value) { - FeatureManager.setEnabled(identifier, value) - } - override val delegateFeature: FirmamentFeature - get() = this - val config: ManagedConfig? get() = null - fun onLoad() {} - -} diff --git a/src/main/kotlin/features/chat/AutoCompletions.kt b/src/main/kotlin/features/chat/AutoCompletions.kt index 9e0de40..f13fe7e 100644 --- a/src/main/kotlin/features/chat/AutoCompletions.kt +++ b/src/main/kotlin/features/chat/AutoCompletions.kt @@ -1,6 +1,15 @@ package moe.nea.firmament.features.chat +import com.mojang.brigadier.Message import com.mojang.brigadier.arguments.StringArgumentType.string +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.BuiltInExceptions +import com.mojang.brigadier.exceptions.CommandExceptionType +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import kotlin.concurrent.thread +import net.minecraft.SharedConstants +import net.minecraft.commands.BrigadierExceptions import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.get import moe.nea.firmament.commands.suggestsList @@ -8,21 +17,21 @@ import moe.nea.firmament.commands.thenArgument import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.events.CommandEvent import moe.nea.firmament.events.MaskCommands -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.tr -object AutoCompletions : FirmamentFeature { +object AutoCompletions { + @Config object TConfig : ManagedConfig(identifier, Category.CHAT) { val provideWarpTabCompletion by toggle("warp-complete") { true } val replaceWarpIsByWarpIsland by toggle("warp-is") { true } } - override val config: ManagedConfig? - get() = TConfig - override val identifier: String + val identifier: String get() = "auto-completions" @Subscribe @@ -44,12 +53,20 @@ object AutoCompletions : FirmamentFeature { thenExecute { val warpName = get(toArg) if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) { - MC.sendServerCommand("warp island") + MC.sendCommand("warp island") } else { - MC.sendServerCommand("warp $warpName") + redirectToServer() } } } } } + + fun CommandContext<*>.redirectToServer() { + val message = tr( + "firmament.warp.auto-complete.internal-throw", + "This is an internal syntax exception that should not show up in gameplay, used to pass on a command to the server" + ) + throw CommandSyntaxException(CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand(), message) + } } diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt index f85825b..aca7af8 100644 --- a/src/main/kotlin/features/chat/ChatLinks.kt +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -1,47 +1,48 @@ 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.URL +import java.net.URI import java.util.Collections import java.util.concurrent.atomic.AtomicInteger -import moe.nea.jarvis.api.Point +import org.joml.Vector2i import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.future.await import kotlin.math.min -import net.minecraft.client.gui.screen.ChatScreen -import net.minecraft.client.texture.NativeImage -import net.minecraft.client.texture.NativeImageBackedTexture -import net.minecraft.text.ClickEvent -import net.minecraft.text.HoverEvent -import net.minecraft.text.Style -import net.minecraft.text.Text -import net.minecraft.util.Formatting -import net.minecraft.util.Identifier +import net.minecraft.client.gui.screens.ChatScreen +import com.mojang.blaze3d.platform.NativeImage +import net.minecraft.client.renderer.texture.DynamicTexture +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.HoverEvent +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ModifyChatEvent import moe.nea.firmament.events.ScreenRenderPostEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.jarvis.JarvisIntegration import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.net.HttpUtil import moe.nea.firmament.util.render.drawTexture import moe.nea.firmament.util.transformEachRecursively import moe.nea.firmament.util.unformattedString -object ChatLinks : FirmamentFeature { - override val identifier: String +object ChatLinks { + val identifier: String get() = "chat-links" + @Config object TConfig : ManagedConfig(identifier, Category.CHAT) { val enableLinks by toggle("links-enabled") { true } val imageEnabled by toggle("image-enabled") { true } val allowAllHosts by toggle("allow-all-hosts") { false } val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" } val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() } - val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) } + val position by position("position", 16 * 20, 9 * 20) { Vector2i(0, 0) } } private fun isHostAllowed(host: String) = @@ -49,12 +50,11 @@ object ChatLinks : FirmamentFeature { private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) - override val config get() = TConfig - val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex() + val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?(\\s|$))".toRegex() val nextTexId = AtomicInteger(0) data class Image( - val texture: Identifier, + val texture: ResourceLocation, val width: Int, val height: Int, ) @@ -71,18 +71,16 @@ object ChatLinks : FirmamentFeature { } imageCache[url] = Firmament.coroutineScope.async { try { - val response = Firmament.httpClient.get(URL(url)) - if (response.status.value == 200) { - val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob) - val image = NativeImage.read(inputStream) - val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") - MC.textureManager.registerTexture( - texId, - NativeImageBackedTexture(image) - ) - Image(texId, image.width, image.height) - } else - null + val inputStream = HttpUtil.request(url) + .forInputStream() + .await() + val image = NativeImage.read(inputStream) + val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") + MC.textureManager.register( + texId, + DynamicTexture({ texId.path }, image) + ) + Image(texId, image.width, image.height) } catch (exc: Exception) { exc.printStackTrace() null @@ -101,19 +99,19 @@ object ChatLinks : FirmamentFeature { if (!TConfig.imageEnabled) return 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 + MC.inGameHud.chat.getClickedComponentStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: 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 if (!imageFuture.isCompleted) return val image = imageFuture.getCompleted() ?: return - it.drawContext.matrices.push() + it.drawContext.pose().pushMatrix() val pos = TConfig.position - pos.applyTransformations(it.drawContext.matrices) + pos.applyTransformations(JarvisIntegration.jarvis, it.drawContext.pose()) val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width)) - it.drawContext.matrices.scale(scale, scale, 1F) + it.drawContext.pose().scale(scale, scale) it.drawContext.drawTexture( image.texture, 0, @@ -125,7 +123,7 @@ object ChatLinks : FirmamentFeature { image.width, image.height, ) - it.drawContext.matrices.pop() + it.drawContext.pose().popMatrix() } @Subscribe @@ -134,23 +132,24 @@ object ChatLinks : FirmamentFeature { it.replaceWith = it.replaceWith.transformEachRecursively { child -> val text = child.string if ("://" !in text) return@transformEachRecursively child - val s = Text.empty().setStyle(child.style) + val s = Component.empty().setStyle(child.style) var index = 0 while (index < text.length) { val nextMatch = urlRegex.find(text, index) - if (nextMatch == null) { - s.append(Text.literal(text.substring(index, text.length))) + val url = nextMatch?.groupValues[0] + val uri = runCatching { url?.let(::URI) }.getOrNull() + if (nextMatch == null || url == null || uri == null) { + s.append(Component.literal(text.substring(index, text.length))) break } val range = nextMatch.groups[0]!!.range - val url = nextMatch.groupValues[0] - s.append(Text.literal(text.substring(index, range.first))) + s.append(Component.literal(text.substring(index, range.first))) s.append( - 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)) + Component.literal(url).setStyle( + Style.EMPTY.withUnderlined(true).withColor( + ChatFormatting.AQUA + ).withHoverEvent(HoverEvent.ShowText(Component.literal(url))) + .withClickEvent(ClickEvent.OpenUrl(uri)) ) ) if (isImageUrl(url)) diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt new file mode 100644 index 0000000..6bef99f --- /dev/null +++ b/src/main/kotlin/features/chat/CopyChat.kt @@ -0,0 +1,21 @@ +package moe.nea.firmament.features.chat + +import net.minecraft.util.FormattedCharSequence +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.reconstitute + + +object CopyChat { + val identifier: String + get() = "copy-chat" + + @Config + object TConfig : ManagedConfig(identifier, Category.CHAT) { + val copyChat by toggle("copy-chat") { false } + } + + fun orderedTextToString(orderedText: FormattedCharSequence): String { + return orderedText.reconstitute().string + } +} diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt index de3a0d9..85daf51 100644 --- a/src/main/kotlin/features/chat/PartyCommands.kt +++ b/src/main/kotlin/features/chat/PartyCommands.kt @@ -5,17 +5,18 @@ 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 net.minecraft.core.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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.tr import moe.nea.firmament.util.useMatch @@ -79,7 +80,7 @@ object PartyCommands { register("coords") { executes { - val p = MC.player?.blockPos ?: BlockPos.ORIGIN + val p = MC.player?.blockPosition() ?: BlockPos.ZERO MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}") 0 } @@ -89,6 +90,7 @@ object PartyCommands { // TODO: at TPS command } + @Config object TConfig : ManagedConfig("party-commands", Category.CHAT) { val enable by toggle("enable") { false } val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds } diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt index 7963171..b857f8a 100644 --- a/src/main/kotlin/features/chat/QuickCommands.kt +++ b/src/main/kotlin/features/chat/QuickCommands.kt @@ -5,9 +5,9 @@ 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 net.minecraft.commands.CommandBuildContext +import net.minecraft.network.protocol.game.ClientboundCommandsPacket +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.DefaultSource import moe.nea.firmament.commands.RestArgumentType @@ -15,18 +15,19 @@ 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.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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.grey import moe.nea.firmament.util.tr -object QuickCommands : FirmamentFeature { - override val identifier: String +object QuickCommands { + val identifier: String get() = "quick-commands" + @Config object TConfig : ManagedConfig("quick-commands", Category.CHAT) { val enableJoin by toggle("join") { true } val enableDh by toggle("dh") { true } @@ -43,10 +44,14 @@ object QuickCommands : FirmamentFeature { val dispatcher = CommandDispatcher<FabricClientCommandSource>() ClientCommandInternals.setActiveDispatcher(dispatcher) ClientCommandRegistrationCallback.EVENT.invoker() - .register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries, - network.enabledFeatures)) + .register( + dispatcher, CommandBuildContext.simple( + network.registryAccess, + network.enabledFeatures() + ) + ) ClientCommandInternals.finalizeInit() - network.onCommandTree(lastPacket) + network.handleCommands(lastPacket) } catch (ex: Exception) { ClientCommandInternals.setActiveDispatcher(fallback) throw ex @@ -64,7 +69,7 @@ object QuickCommands : FirmamentFeature { return lf } - var lastReceivedTreePacket: CommandTreeS2CPacket? = null + var lastReceivedTreePacket: ClientboundCommandsPacket? = null val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") @@ -98,16 +103,20 @@ object QuickCommands : FirmamentFeature { } val joinName = getNameForFloor(what.replace(" ", "").lowercase()) if (joinName == null) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what)) + source.sendFeedback(Component.translatableEscape("firmament.quick-commands.join.unknown", what)) } else { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success", - joinName)) + source.sendFeedback( + Component.translatableEscape( + "firmament.quick-commands.join.success", + joinName + ) + ) MC.sendCommand("joininstance $joinName") } } } thenExecute { - source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain")) + source.sendFeedback(Component.translatable("firmament.quick-commands.join.explain")) } } } @@ -122,8 +131,12 @@ object QuickCommands : FirmamentFeature { ) } if (l !in kuudraLevelNames.indices) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra", - kuudraLevel)) + source.sendFeedback( + Component.translatableEscape( + "firmament.quick-commands.join.unknown-kuudra", + kuudraLevel + ) + ) return null } return "KUUDRA_${kuudraLevelNames[l]}" @@ -143,8 +156,12 @@ object QuickCommands : FirmamentFeature { return "CATACOMBS_ENTRANCE" } if (l !in dungeonLevelNames.indices) { - source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs", - kuudraLevel)) + source.sendFeedback( + Component.translatableEscape( + "firmament.quick-commands.join.unknown-catacombs", + kuudraLevel + ) + ) return null } return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}" diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..dc77115 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,193 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.commands.arguments.ResourceKeyArgument +import net.minecraft.core.component.DataComponentType +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.Tag +import net.minecraft.nbt.NbtOps +import net.minecraft.core.registries.Registries +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: DataComponentType<T>, + ) { + fun observe(itemStack: ItemStack): Collection<Tag> { + 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(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + 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.level.getEntitiesOfClass( + ArmorStand::class.java, + p.boundingBox.inflate(10.0), + { it.isMarker }) + .minBy { it.distanceToSqr(p) } + toggleObserve(nearestPet) + } + } + thenExecute { + val ent = MC.instance.crosshairPickEntity + if (ent == null) { + source.sendFeedback( + tr( + "firmament.fitstealer.notargetundercursor", + "No entity under cursor" + ) + ) + } else { + toggleObserve(ent) + } + } + } + thenLiteral("path") { + thenArgument( + "component", + ResourceKeyArgument.key(Registries.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).location()}" + ) + ) + } + } + } + } + } + } + } + + 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..2c8ced0 100644 --- a/src/main/kotlin/features/debug/DebugLogger.kt +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -1,24 +1,29 @@ package moe.nea.firmament.features.debug import kotlinx.serialization.serializer -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TestUtil import moe.nea.firmament.util.collections.InstanceList +import moe.nea.firmament.util.data.Config import moe.nea.firmament.util.data.DataHolder class DebugLogger(val tag: String) { companion object { val allInstances = InstanceList<DebugLogger>("DebugLogger") } + + @Config object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf) init { allInstances.add(this) } - fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag) + fun isEnabled() = TestUtil.isInTest || EnabledLogs.data.contains(tag) + fun log(text: String) = log { text } fun log(text: () -> String) { if (!isEnabled()) return - MC.sendChat(Text.literal(text())) + MC.sendChat(Component.literal(text())) } } diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt deleted file mode 100644 index ee54260..0000000 --- a/src/main/kotlin/features/debug/DebugView.kt +++ /dev/null @@ -1,29 +0,0 @@ - - -package moe.nea.firmament.features.debug - -import moe.nea.firmament.Firmament -import moe.nea.firmament.annotations.Subscribe -import moe.nea.firmament.events.TickEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.util.TimeMark - -object DebugView : FirmamentFeature { - private data class StoredVariable<T>( - val obj: T, - val timer: TimeMark, - ) - - private val storedVariables: MutableMap<String, StoredVariable<*>> = sortedMapOf() - override val identifier: String - get() = "debug-view" - override val defaultEnabled: Boolean - get() = Firmament.DEBUG - - fun <T : Any?> showVariable(label: String, obj: T) { - synchronized(this) { - storedVariables[label] = StoredVariable(obj, TimeMark.now()) - } - } - -} diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt index 8f0c25c..8638bb6 100644 --- a/src/main/kotlin/features/debug/DeveloperFeatures.kt +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -3,33 +3,38 @@ 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 -import net.minecraft.client.MinecraftClient -import net.minecraft.text.Text +import net.minecraft.client.Minecraft +import net.minecraft.network.chat.Component 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.iterate -object DeveloperFeatures : FirmamentFeature { - override val identifier: String +object DeveloperFeatures { + val DEVELOPER_SUBCOMMAND: String = "dev" + val identifier: String get() = "developer" - override val config: TConfig - get() = TConfig - override val defaultEnabled: Boolean - get() = Firmament.DEBUG val gradleDir = Path.of(".").absolute() .iterate { it.parent } .find { it.resolve("settings.gradle.kts").exists() } + @Config object TConfig : ManagedConfig("developer", Category.DEV) { val autoRebuildResources by toggle("auto-rebuild") { false } } @@ -42,6 +47,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 @@ -51,24 +92,27 @@ object DeveloperFeatures : FirmamentFeature { } @JvmStatic - fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture<Void> { - val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) { + fun hookOnBeforeResourceReload(client: Minecraft): CompletableFuture<Void> { + val reloadFuture = if (TConfig.autoRebuildResources && Firmament.DEBUG && gradleDir != null) { val builder = ProcessBuilder("./gradlew", ":processResources") builder.directory(gradleDir.toFile()) builder.inheritIO() val process = builder.start() - MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start")) + MC.sendChat(Component.translatable("firmament.dev.resourcerebuild.start")) val startTime = TimeMark.now() process.toHandle().onExit().thenApply { - MC.sendChat(Text.stringifiedTranslatable( - "firmament.dev.resourcerebuild.done", - startTime.passedTime())) + MC.sendChat( + Component.translatableEscape( + "firmament.dev.resourcerebuild.done", + startTime.passedTime() + ) + ) Unit } } else { CompletableFuture.completedFuture(Unit) } - return reloadFuture.thenCompose { client.reloadResources() } + return reloadFuture.thenCompose { client.reloadResourcePacks() } } } diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt new file mode 100644 index 0000000..a2b42fd --- /dev/null +++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.debug + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional +import net.minecraft.SharedConstants +import moe.nea.firmament.Firmament + +data class ExportedTestConstantMeta( + val dataVersion: Int, + val modVersion: Optional<String>, +) { + companion object { + val current = ExportedTestConstantMeta( + SharedConstants.getCurrentVersion().dataVersion().version, + Optional.of("Firmament ${Firmament.version.friendlyString}") + ) + + 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) + } + val SOURCE_CODEC = CODEC.fieldOf("source").codec() + } +} diff --git a/src/main/kotlin/features/debug/MinorTrolling.kt b/src/main/kotlin/features/debug/MinorTrolling.kt index 32035a6..7936521 100644 --- a/src/main/kotlin/features/debug/MinorTrolling.kt +++ b/src/main/kotlin/features/debug/MinorTrolling.kt @@ -2,15 +2,13 @@ package moe.nea.firmament.features.debug -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ModifyChatEvent -import moe.nea.firmament.features.FirmamentFeature - // In memorian Dulkir -object MinorTrolling : FirmamentFeature { - override val identifier: String +object MinorTrolling { + val identifier: String get() = "minor-trolling" val trollers = listOf("nea89o", "lrg89") @@ -22,6 +20,6 @@ object MinorTrolling : FirmamentFeature { val (_, name, text) = m.groupValues if (name !in trollers) return if (!text.startsWith("c:")) return - it.replaceWith = Text.literal(text.substring(2).replace("&", "§")) + it.replaceWith = Component.literal(text.substring(2).replace("&", "§")) } } diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 225bc13..145ea35 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -2,43 +2,55 @@ package moe.nea.firmament.features.debug import com.mojang.serialization.JsonOps 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.LivingEntity -import net.minecraft.item.ItemStack -import net.minecraft.item.Items +import net.minecraft.world.level.block.SkullBlock +import net.minecraft.world.level.block.entity.SkullBlockEntity +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtOps -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.advancements.critereon.NbtPredicate +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.Nameable +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.EntityHitResult +import net.minecraft.world.phys.HitResult import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldKeyboardEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.grey +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.mc.unsafeNbt import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr -object PowerUserTools : FirmamentFeature { - override val identifier: String +object PowerUserTools { + val identifier: String get() = "power-user" + @Config object TConfig : ManagedConfig(identifier, Category.DEV) { val showItemIds by toggle("show-item-id") { false } val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") @@ -48,22 +60,26 @@ 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") + val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack") + val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe") + val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location") + val highlightNonOverlayItems by toggle("highlight-non-overlay") { false } + val dontHighlightSemicolonItems by toggle("dont-highlight-semicolon-items") { false } + val showSlotNumbers by keyBindingWithDefaultUnbound("slot-numbers") + val autoCopyAnimatedSkins by toggle("copy-animated-skins") { false } } - override val config - get() = TConfig - - var lastCopiedStack: Pair<ItemStack, Text>? = null + var lastCopiedStack: Pair<ItemStack, Component>? = null set(value) { field = value - if (value != null) lastCopiedStackViewTime = true + if (value != null) lastCopiedStackViewTime = 2 } - var lastCopiedStackViewTime = false + var lastCopiedStackViewTime = 0 @Subscribe fun resetLastCopiedStack(event: TickEvent) { - if (!lastCopiedStackViewTime) lastCopiedStack = null - lastCopiedStackViewTime = false + if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null } @Subscribe @@ -71,39 +87,58 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = null } - fun debugFormat(itemStack: ItemStack): Text { - return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + fun debugFormat(itemStack: ItemStack): Component { + return Component.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + } + + @Subscribe + fun onRender(event: SlotRenderEvents.After) { + if (TConfig.showSlotNumbers.isPressed()) { + event.context.drawString( + MC.font, + event.slot.index.toString(), event.slot.x, event.slot.y, 0xFF00FF00.toInt(), true + ) + event.context.drawString( + MC.font, + event.slot.containerSlot.toString(), event.slot.x, event.slot.y + MC.font.lineHeight, 0xFFFF0000.toInt(), true + ) + } } @Subscribe fun onEntityInfo(event: WorldKeyboardEvent) { if (!event.matches(TConfig.copyEntityData)) return - val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity + val target = (MC.instance.hitResult as? EntityHitResult)?.entity if (target == null) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.fail")) + MC.sendChat(Component.translatable("firmament.poweruser.entity.fail")) return } showEntity(target) } fun showEntity(target: Entity) { - 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)) + val nbt = NbtPredicate.getEntityTagToCompare(target) + nbt.remove("Inventory") + nbt.put("StyledName", ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, target.feedbackDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) + MC.sendChat(Component.translatable("firmament.poweruser.entity.type", target.type)) + MC.sendChat(Component.translatable("firmament.poweruser.entity.name", target.name)) + MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.position", target.position)) if (target is LivingEntity) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) - for (armorItem in target.armorItems) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) + MC.sendChat(Component.translatable("firmament.poweruser.entity.armor")) + for ((slot, armorItem) in target.iterableArmorItems) { + MC.sendChat(Component.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) } } - MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size)) - target.passengerList.forEach { + MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.passengers", target.passengers.size)) + target.passengers.forEach { showEntity(it) } } // TODO: leak this through some other way, maybe. - lateinit var getSkullId: (profile: ProfileComponent) -> Identifier? + lateinit var getSkullId: (profile: ResolvableProfile) -> ResourceLocation? @Subscribe fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) { @@ -112,58 +147,74 @@ object PowerUserTools : FirmamentFeature { if (it.matches(TConfig.copyItemId)) { val sbId = item.skyBlockId if (sbId == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skyblockid.fail")) return } ClipboardUtils.setTextContent(sbId.neuItem) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) + Pair(item, Component.translatableEscape("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: ResourceLocation): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe if (model == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.modelid.fail")) return } ClipboardUtils.setTextContent(model.toString()) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString())) + Pair(item, Component.translatableEscape("firmament.tooltip.copied.modelid", model.toString())) } else if (it.matches(TConfig.copyNbtData)) { // TODO: copy full nbt - val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "<empty>" + val nbt = item.get(DataComponents.CUSTOM_DATA)?.unsafeNbt?.toPrettyString() ?: "<empty>" ClipboardUtils.setTextContent(nbt) - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.nbt")) } else if (it.matches(TConfig.copyLoreData)) { val list = mutableListOf(item.displayNameAccordingToNbt) list.addAll(item.loreAccordingToNbt) ClipboardUtils.setTextContent(list.joinToString("\n") { - TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString() + ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString() }) - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.lore")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.lore")) } else if (it.matches(TConfig.copySkullTexture)) { if (item.item != Items.PLAYER_HEAD) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) return } - val profile = item.get(DataComponentTypes.PROFILE) + val profile = item.get(DataComponents.PROFILE) if (profile == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) return } val skullTexture = getSkullId(profile) if (skullTexture == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) return } ClipboardUtils.setTextContent(skullTexture.toString()) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())) + Pair(item, Component.translatableEscape("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()) - lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) + val nbt = ItemStack.CODEC + .encodeStart(MC.currentOrDefaultRegistries.createSerializationContext(NbtOps.INSTANCE), item) + .orThrow + ClipboardUtils.setTextContent(nbt.toPrettyString()) + lastCopiedStack = Pair(item, Component.translatableEscape("firmament.tooltip.copied.stack")) + } else if (it.matches(TConfig.copyTitle) && it.screen is AbstractContainerScreen<*>) { + val allTitles = ListTag() + val inventoryNames = + it.screen.menu.slots + .mapNotNullTo(mutableSetOf()) { it.container } + .filterIsInstance<Nameable>() + .map { it.name } + for (it in listOf(it.screen.title) + inventoryNames) { + allTitles.add(ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!) + } + ClipboardUtils.setTextContent(allTitles.toPrettyString()) + MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles")) } } @@ -171,23 +222,23 @@ object PowerUserTools : FirmamentFeature { fun onCopyWorldInfo(it: WorldKeyboardEvent) { if (it.matches(TConfig.copySkullTexture)) { val p = MC.camera ?: return - val blockHit = p.raycast(20.0, 0.0f, false) ?: return + val blockHit = p.pick(20.0, 0.0f, false) ?: return if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) return } - val blockAt = p.world.getBlockState(blockHit.blockPos)?.block - val entity = p.world.getBlockEntity(blockHit.blockPos) - if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.owner == null) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + val blockAt = p.level.getBlockState(blockHit.blockPos)?.block + val entity = p.level.getBlockEntity(blockHit.blockPos) + if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.ownerProfile == null) { + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) return } - val id = getSkullId(entity.owner!!) + val id = getSkullId(entity.ownerProfile!!) if (id == null) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) } else { ClipboardUtils.setTextContent(id.toString()) - MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString())) + MC.sendChat(Component.translatableEscape("firmament.tooltip.copied.skull", id.toString())) } } } @@ -196,14 +247,14 @@ object PowerUserTools : FirmamentFeature { fun addItemId(it: ItemTooltipEvent) { if (TConfig.showItemIds) { val id = it.stack.skyBlockId ?: return - it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem)) + it.lines.add(Component.translatableEscape("firmament.tooltip.skyblockid", id.neuItem).grey()) } val (item, text) = lastCopiedStack ?: return - if (!ItemStack.areEqual(item, it.stack)) { + if (!ItemStack.matches(item, it.stack)) { lastCopiedStack = null return } - lastCopiedStackViewTime = true + lastCopiedStackViewTime = 0 it.lines.add(text) } diff --git a/src/main/kotlin/features/debug/SkinPreviews.kt b/src/main/kotlin/features/debug/SkinPreviews.kt new file mode 100644 index 0000000..56c63db --- /dev/null +++ b/src/main/kotlin/features/debug/SkinPreviews.kt @@ -0,0 +1,91 @@ +package moe.nea.firmament.features.debug + +import com.mojang.authlib.GameProfile +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.time.Duration.Companion.seconds +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.phys.Vec3 +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.IsSlotProtectedEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.json.toJsonArray +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.rawSkyBlockId +import moe.nea.firmament.util.toTicks +import moe.nea.firmament.util.tr + + +object SkinPreviews { + + // TODO: add pet support + @Subscribe + fun onEntityUpdate(event: EntityUpdateEvent) { + if (!isRecording) return + if (event.entity.position != pos) + return + val entity = event.entity as? LivingEntity ?: return + val stack = entity.getItemBySlot(EquipmentSlot.HEAD) ?: return + val profile = stack.get(DataComponents.PROFILE)?.partialProfile() ?: return + if (profile == animation.lastOrNull()) return + animation.add(profile) + val shortened = animation.shortenCycle() + if (shortened.size <= (animation.size / 2).coerceAtLeast(1) && lastDiscard.passedTime() > 2.seconds) { + val tickEstimation = (lastDiscard.passedTime() / animation.size).toTicks() + val skinName = if (skinColor != null) "${skinId}_${skinColor?.replace(" ", "_")?.uppercase()}" else skinId!! + val json = + buildJsonObject { + put("ticks", tickEstimation) + put( + "textures", + shortened.map { + it.id.toString() + ":" + it.properties()["textures"].first().value() + }.toJsonArray() + ) + } + MC.sendChat( + tr( + "firmament.dev.skinpreviews.done", + "Observed a total of ${animation.size} elements, which could be shortened to a cycle of ${shortened.size}. Copying JSON array. Estimated ticks per frame: $tickEstimation." + ) + ) + isRecording = false + ClipboardUtils.setTextContent(JsonPrimitive(skinName).toString() + ":" + json.toString()) + } + } + + var animation = mutableListOf<GameProfile>() + var pos = Vec3(-1.0, 72.0, -101.25) + var isRecording = false + var skinColor: String? = null + var skinId: String? = null + var lastDiscard = TimeMark.farPast() + + @Subscribe + fun onActivate(event: IsSlotProtectedEvent) { + if (!PowerUserTools.TConfig.autoCopyAnimatedSkins) return + val lastLine = event.itemStack.loreAccordingToNbt.lastOrNull()?.string + if (lastLine != "Right-click to preview!" && lastLine != "Click to preview!") return + lastDiscard = TimeMark.now() + val stackName = event.itemStack.displayNameAccordingToNbt.string + if (stackName == "FIRE SALE!") { + skinColor = null + skinId = event.itemStack.rawSkyBlockId + } else { + skinColor = stackName + } + animation.clear() + isRecording = true + MC.sendChat(tr("firmament.dev.skinpreviews.start", "Starting to observe items")) + } +} diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt new file mode 100644 index 0000000..37b248a --- /dev/null +++ b/src/main/kotlin/features/debug/SoundVisualizer.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.network.chat.Component +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.SoundReceiveEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.red +import moe.nea.firmament.util.render.RenderInWorldContext + +object SoundVisualizer { + + var showSounds = false + + var sounds = mutableListOf<SoundReceiveEvent>() + + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("sounds") { + thenExecute { + showSounds = !showSounds + if (!showSounds) { + sounds.clear() + } + } + } + } + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + sounds.clear() + } + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + RenderInWorldContext.renderInWorld(event) { + sounds.forEach { event -> + withFacingThePlayer(event.position) { + text( + Component.literal(event.sound.value().location.toString()).also { + if (event.cancelled) + it.red() + }, + verticalAlign = RenderInWorldContext.VerticalAlign.CENTER, + ) + } + } + } + } + + @Subscribe + fun onSoundReceive(event: SoundReceiveEvent) { + if (!showSounds) return + if (sounds.size > 1000) { + sounds.subList(0, 200).clear() + } + sounds.add(event) + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt new file mode 100644 index 0000000..d12d667 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt @@ -0,0 +1,256 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import net.minecraft.client.player.AbstractClientPlayer +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.core.ClientAsset +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.repo.ItemNameLookup +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.async.waitForTextInput +import moe.nea.firmament.util.ifDropLast +import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.setSkullOwner +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.red +import moe.nea.firmament.util.removeColorCodes +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object ExportRecipe { + + + val xNames = "123" + val yNames = "ABC" + + val slotIndices = (0..<9).map { + val x = it % 3 + val y = it / 3 + + (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10 + } + val resultSlot = 25 + val craftingTableSlut = resultSlot - 2 + + @Subscribe + fun exportNpcLocation(event: WorldKeyboardEvent) { + if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) { + return + } + val entity = MC.instance.crosshairPickEntity + if (entity == null) { + MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export")) + return + } + Firmament.coroutineScope.launch { + val guessName = entity.level.getEntitiesOfClass( + ArmorStand::class.java, + entity.boundingBox.inflate(0.1), + { !it.name.string.contains("CLICK") }) + .firstOrNull()?.customName?.string + ?: "" + val reply = waitForTextInput("$guessName (NPC)", "Export stub") + val id = generateName(reply) + ItemExporter.exportStub(id, "§9$reply") { + val playerEntity = entity as? AbstractClientPlayer + val textureUrl = (playerEntity?.skin?.body as? ClientAsset.DownloadedTexture)?.url + if (textureUrl != null) + it.setSkullOwner(playerEntity.uuid, textureUrl) + } + ItemExporter.modifyJson(id) { + val mutJson = it.toMutableMap() + mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown") + mutJson["x"] = JsonPrimitive(entity.blockX) + mutJson["y"] = JsonPrimitive(entity.blockY) + mutJson["z"] = JsonPrimitive(entity.blockZ) + JsonObject(mutJson) + } + } + } + + @Subscribe + fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) { + if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) { + return + } + val title = event.screen.title.string + val sellSlot = event.screen.getSlotByIndex(49, false)?.item + val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false) + if (craftingTableSlot?.item?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") { + slotIndices.forEach { (_, index) -> + event.screen.getSlotByIndex(index, false)?.item?.let(ItemExporter::ensureExported) + } + val inputs = slotIndices.associate { (name, index) -> + val id = event.screen.getSlotByIndex(index, false)?.item?.takeIf { !it.isEmpty() }?.let { + "${it.skyBlockId?.neuItem}:${it.count}" + } ?: "" + name to JsonPrimitive(id) + } + val output = event.screen.getSlotByIndex(resultSlot, false)?.item!! + val overrideOutputId = output.skyBlockId!!.neuItem + val count = output.count + val recipe = JsonObject( + inputs + mapOf( + "type" to JsonPrimitive("crafting"), + "count" to JsonPrimitive(count), + "overrideOutputId" to JsonPrimitive(overrideOutputId) + ) + ) + ItemExporter.appendRecipe(output.skyBlockId!!, recipe) + MC.sendChat(tr("firmament.repo.export.recipe", "Recipe for ${output.skyBlockId} exported.")) + return + } else if (sellSlot?.displayNameAccordingToNbt?.string == "Sell Item" || (sellSlot?.loreAccordingToNbt + ?: listOf()).any { it.string == "Click to buyback!" } + ) { + val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC") + if (!ItemExporter.isExported(shopId)) { + // TODO: export location + skin of last clicked npc + ItemExporter.exportStub(shopId, "§9$title (NPC)") + } + for (index in (9..9 * 5)) { + val item = event.screen.getSlotByIndex(index, false)?.item ?: continue + val skyblockId = item.skyBlockId ?: continue + val costLines = item.loreAccordingToNbt + .map { it.string.trim() } + .dropWhile { !it.startsWith("Cost") } + .dropWhile { it == "Cost" } + .takeWhile { it != "Click to trade!" } + .takeWhile { it != "Stock" } + .filter { !it.isBlank() } + .map { it.removePrefix("Cost: ") } + + + val costs = costLines.mapNotNull { lineText -> + val line = findStackableItemByName(lineText) + if (line == null) { + MC.sendChat( + tr( + "firmament.repo.itemshop.fail", + "Could not parse cost item ${lineText} for ${item.displayNameAccordingToNbt}" + ).red() + ) + } + line + } + + + ItemExporter.appendRecipe( + shopId, JsonObject( + mapOf( + "type" to JsonPrimitive("npc_shop"), + "cost" to JsonArray(costs.map { JsonPrimitive("${it.first.neuItem}:${it.second}") }), + "result" to JsonPrimitive("${skyblockId.neuItem}:${item.count}"), + ) + ) + ) + } + MC.sendChat(tr("firmament.repo.export.itemshop", "Item Shop export for ${title} complete.")) + } else { + MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found")) + } + } + + private val coinRegex = "(?<amount>$SHORT_NUMBER_FORMAT) Coins?".toPattern() + private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_FORMAT)".toPattern() + private val reverseStackedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT)x (?<name>.*)".toPattern() + private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_FORMAT)".toPattern() + private val numberedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT) (?<what>.*)".toPattern() + + private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_FORMAT})x? (?<what>.*)".toPattern() + + fun findForName(name: String, fallbackToGenerated: Boolean = true): SkyblockId? { + var id = ItemNameLookup.guessItemByName(name, true) + if (id == null && fallbackToGenerated) { + id = generateName(name) + } + return id + } + + fun skill(name: String): SkyblockId { + return SkyblockId("SKYBLOCK_SKILL_${name}") + } + + fun generateName(name: String): SkyblockId { + return SkyblockId(name.uppercase().replace(" ", "_").replace(Regex("[^A-Z_]+"), "")) + } + + fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? { + val properName = name.removeColorCodes().trim() + if (properName == "FREE" || properName == "This Chest is Free!") { + return Pair(SkyBlockItems.COINS, 0.0) + } + coinRegex.useMatch(properName) { + return Pair(SkyBlockItems.COINS, parseShortNumber(group("amount"))) + } + etherialRewardPattern.useMatch(properName) { + val id = when (val id = group("what")) { + "Copper" -> SkyblockId("SKYBLOCK_COPPER") + "Bits" -> SkyblockId("SKYBLOCK_BIT") + "Garden Experience" -> SkyblockId("SKYBLOCK_SKILL_GARDEN") + "Farming XP" -> SkyblockId("SKYBLOCK_SKILL_FARMING") + "Gold Essence" -> SkyblockId("ESSENCE_GOLD") + "Gemstone Powder" -> SkyblockId("SKYBLOCK_POWDER_GEMSTONE") + "Mithril Powder" -> SkyblockId("SKYBLOCK_POWDER_MITHRIL") + "Pelts" -> SkyblockId("SKYBLOCK_PELT") + "Fine Flour" -> SkyblockId("FINE_FLOUR") + else -> { + id.ifDropLast(" Experience") { + skill(generateName(it).neuItem) + } ?: id.ifDropLast(" XP") { + skill(generateName(it).neuItem) + } ?: id.ifDropLast(" Powder") { + SkyblockId("SKYBLOCK_POWDER_${generateName(it).neuItem}") + } ?: id.ifDropLast(" Essence") { + SkyblockId("ESSENCE_${generateName(it).neuItem}") + } ?: generateName(id) + } + } + return Pair(id, parseShortNumber(group("amount"))) + } + essenceRegex.useMatch(properName) { + return Pair( + SkyblockId("ESSENCE_${group("essence").uppercase()}"), + parseShortNumber(group("count")) + ) + } + stackedItemRegex.useMatch(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + reverseStackedItemRegex.useMatch(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + numberedItemRegex.useMatch(properName) { + val item = findForName(group("what"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + + return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) } + } + +} diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt new file mode 100644 index 0000000..f664da3 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt @@ -0,0 +1,250 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.notExists +import kotlin.io.path.readText +import kotlin.io.path.relativeTo +import kotlin.io.path.writeText +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.StringTag +import net.minecraft.network.chat.Component +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +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.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.repo.RepoDownloadManager +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.LegacyTagParser +import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.toNbtList +import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.setSkyBlockId +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr + +object ItemExporter { + + fun exportItem(itemStack: ItemStack): Component { + nonOverlayCache.clear() + val exporter = LegacyItemExporter.createExporter(itemStack) + var json = exporter.exportJson() + val fileName = json.jsonObject["internalname"]?.jsonPrimitive?.takeIf { it.isString }?.content + if (fileName == null) { + return tr( + "firmament.repoexport.nointernalname", + "Could not find internal name to export for this item (null.json)" + ) + } + val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json") + itemFile.createParentDirectories() + if (itemFile.exists()) { + val existing = try { + Firmament.json.decodeFromString<JsonObject>(itemFile.readText()) + } catch (ex: Exception) { + ex.printStackTrace() + JsonObject(mapOf()) + } + val mut = json.jsonObject.toMutableMap() + for (prop in existing) { + if (prop.key !in mut || mut[prop.key]!!.let { + (it is JsonPrimitive && (it.content.isEmpty() || it.content == "0")) || (it is JsonArray && it.isEmpty()) || (it is JsonObject && it.isEmpty()) + }) + mut[prop.key] = prop.value + } + json = JsonObject(mut) + } + val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json) + itemFile.writeText(jsonFormatted) + val overlayFile = RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay") + .resolve(ExportedTestConstantMeta.current.dataVersion.toString()) + .resolve("${fileName}.snbt") + overlayFile.createParentDirectories() + overlayFile.writeText(exporter.exportModernSnbt().toPrettyString()) + return tr( + "firmament.repoexport.success", + "Exported item to ${itemFile.relativeTo(RepoDownloadManager.repoSavedLocation)}${ + exporter.warnings.joinToString( + "" + ) { "\nWarning: $it" } + }" + ) + } + + fun pathFor(skyBlockId: SkyblockId) = + RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json") + + fun isExported(skyblockId: SkyblockId) = + pathFor(skyblockId).exists() + + fun ensureExported(itemStack: ItemStack) { + if (!isExported(itemStack.skyBlockId ?: return)) + MC.sendChat(exportItem(itemStack)) + } + + fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) { + val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText()) + val newJson = modify(oldJson) + pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson))) + } + + fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) { + modifyJson(skyblockId) { oldJson -> + val mutableJson = oldJson.toMutableMap() + val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList() + recipes.add(recipe) + mutableJson["recipes"] = JsonArray(recipes) + JsonObject(mutableJson) + } + } + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("reexportlore") { + thenArgument("itemid", RestArgumentType) { itemid -> + suggests { ctx, builder -> + val spaceIndex = builder.remaining.lastIndexOf(" ") + val (before, after) = + if (spaceIndex < 0) Pair("", builder.remaining) + else Pair( + builder.remaining.substring(0, spaceIndex + 1), + builder.remaining.substring(spaceIndex + 1) + ) + RepoManager.neuRepo.items.items.keys + .asSequence() + .filter { it.startsWith(after, ignoreCase = true) } + .forEach { + builder.suggest(before + it) + } + + builder.buildFuture() + } + thenExecute { + for (itemid in get(itemid).split(" ").map { SkyblockId(it) }) { + if (pathFor(itemid).notExists()) { + MC.sendChat( + tr( + "firmament.repo.export.relore.fail", + "Could not find json file to relore for ${itemid}" + ) + ) + } + fixLoreNbtFor(itemid) + MC.sendChat( + tr( + "firmament.repo.export.relore", + "Updated lore / display name for $itemid" + ) + ) + } + } + } + thenLiteral("all") { + thenExecute { + var i = 0 + val chunkSize = 100 + val items = RepoManager.neuRepo.items.items.keys + Firmament.coroutineScope.launch { + items.chunked(chunkSize).forEach { key -> + MC.sendChat( + tr( + "firmament.repo.export.relore.progress", + "Updated lore / display for ${i * chunkSize} / ${items.size}." + ) + ) + i++ + key.forEach { + fixLoreNbtFor(SkyblockId(it)) + } + } + MC.sendChat(tr("firmament.repo.export.relore.alldone", "All lores updated.")) + } + } + } + } + } + } + + fun fixLoreNbtFor(itemid: SkyblockId) { + modifyJson(itemid) { + val mutJson = it.toMutableMap() + val legacyTag = LegacyTagParser.parse(mutJson["nbttag"]!!.jsonPrimitive.content) + val display = legacyTag.getCompoundOrEmpty("display") + legacyTag.put("display", display) + display.putString("Name", mutJson["displayname"]!!.jsonPrimitive.content) + display.put( + "Lore", + (mutJson["lore"] as JsonArray).map { StringTag.valueOf(it.jsonPrimitive.content) } + .toNbtList() + ) + mutJson["nbttag"] = JsonPrimitive(legacyTag.toLegacyString()) + JsonObject(mutJson) + } + } + + @Subscribe + fun onKeyBind(event: HandledScreenKeyPressedEvent) { + if (event.matches(PowerUserTools.TConfig.exportItemStackToRepo)) { + val itemStack = event.screen.focusedItemStack ?: return + PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack)) + } + } + + val nonOverlayCache = mutableMapOf<SkyblockId, Boolean>() + + @Subscribe + fun onRender(event: SlotRenderEvents.Before) { + if (!PowerUserTools.TConfig.highlightNonOverlayItems) { + return + } + val stack = event.slot.item ?: return + val id = event.slot.item.skyBlockId?.neuItem + if (PowerUserTools.TConfig.dontHighlightSemicolonItems && id != null && id.contains(";")) return + val sbId = stack.skyBlockId ?: return + val isExported = nonOverlayCache.getOrPut(sbId) { + RepoManager.overlayData.getOverlayFiles(sbId).isNotEmpty() || // This extra case is here so that an export works immediately, without repo reload + RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay") + .resolve(ExportedTestConstantMeta.current.dataVersion.toString()) + .resolve("${stack.skyBlockId}.snbt") + .exists() + } + if (!isExported) + event.context.drawGuiTexture( + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, + ) + } + + fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) { + exportItem(ItemStack(Items.PLAYER_HEAD).also { + it.displayNameAccordingToNbt = Component.literal(title) + it.loreAccordingToNbt = listOf(Component.literal("")) + it.setSkyBlockId(skyblockId) + extra(it) // LOL + }) + MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId")) + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt new file mode 100644 index 0000000..2bc51bc --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt @@ -0,0 +1,87 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.Serializable +import net.minecraft.nbt.CompoundTag +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.util.StringUtil.camelWords +import moe.nea.firmament.util.mc.loadItemFromNbt + +/** + * Load data based on [prismarine.js' 1.8 item data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.8/items.json) + */ +object LegacyItemData { + @Serializable + data class ItemData( + val id: Int, + val name: String, + val displayName: String, + val stackSize: Int, + val variations: List<Variation> = listOf() + ) { + val properId = if (name.contains(":")) name else "minecraft:$name" + + fun allVariants() = + variations.map { LegacyItemType(properId, it.metadata.toShort()) } + LegacyItemType(properId, 0) + } + + @Serializable + data class Variation( + val metadata: Int, val displayName: String + ) + + data class LegacyItemType( + val name: String, + val metadata: Short + ) { + override fun toString(): String { + return "$name:$metadata" + } + } + + @Serializable + data class EnchantmentData( + val id: Int, + val name: String, + val displayName: String, + ) + + inline fun <reified T : Any> getLegacyData(name: String) = + Firmament.tryDecodeJsonFromStream<T>( + LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!! + ).getOrThrow() + + val enchantmentData = getLegacyData<List<EnchantmentData>>("enchantments") + val enchantmentLut = enchantmentData.associateBy { ResourceLocation.withDefaultNamespace(it.name) } + + val itemDat = getLegacyData<List<ItemData>>("items") + + @OptIn(ExpensiveItemCacheApi::class) // This is fine, we get loaded in a thread. + val itemLut = itemDat.flatMap { item -> + item.allVariants().map { legacyItemType -> + val nbt = ItemCache.convert189ToModern(CompoundTag().apply { + putString("id", legacyItemType.name) + putByte("Count", 1) + putShort("Damage", legacyItemType.metadata) + })!! + nbt.remove("components") + val stack = loadItemFromNbt(nbt) ?: error("Could not transform $legacyItemType: $nbt") + stack.item to legacyItemType + } + }.toMap() + + @Serializable + data class LegacyEffect( + val id: Int, + val name: String, + val displayName: String, + val type: String + ) + + val effectList = getLegacyData<List<LegacyEffect>>("effects") + .associateBy { + it.name.camelWords().map { it.trim().lowercase() }.joinToString("_") + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt new file mode 100644 index 0000000..7b855d1 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt @@ -0,0 +1,318 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.concurrent.thread +import kotlin.jvm.optionals.getOrNull +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.StringTag +import net.minecraft.tags.ItemTags +import net.minecraft.network.chat.Component +import net.minecraft.util.Unit +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.HypixelPetInfo +import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.StringUtil.words +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.getLegacyFormatString +import moe.nea.firmament.util.json.toJsonArray +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.toNbtList +import moe.nea.firmament.util.modifyExtraAttributes +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.transformEachRecursively +import moe.nea.firmament.util.unformattedString + +class LegacyItemExporter private constructor(var itemStack: ItemStack) { + init { + require(!itemStack.isEmpty) + itemStack.count = 1 + } + + var lore = itemStack.loreAccordingToNbt + val originalId = itemStack.extraAttributes.getString("id") + var name = itemStack.displayNameAccordingToNbt + val extraAttribs = itemStack.extraAttributes.copy() + val legacyNbt = CompoundTag() + val warnings = mutableListOf<String>() + + // TODO: check if lore contains non 1.8.9 able hex codes and emit lore in overlay files if so + + fun preprocess() { + // TODO: split up preprocess steps into preprocess actions that can be toggled in a ui + extraAttribs.remove("timestamp") + extraAttribs.remove("uuid") + extraAttribs.remove("modifier") + extraAttribs.getString("petInfo").ifPresent { petInfoJson -> + var petInfo = Firmament.json.decodeFromString<HypixelPetInfo>(petInfoJson) + petInfo = petInfo.copy(candyUsed = 0, heldItem = null, exp = 0.0, active = null, uuid = null) + extraAttribs.putString("petInfo", Firmament.tightJson.encodeToString(petInfo)) + } + itemStack.skyBlockId?.let { + extraAttribs.putString("id", it.neuItem) + } + trimLore() + itemStack.loreAccordingToNbt = itemStack.item.defaultInstance.loreAccordingToNbt + itemStack.remove(DataComponents.CUSTOM_NAME) + } + + fun trimLore() { + val rarityIdx = lore.indexOfLast { + val firstWordInLine = it.unformattedString.words().filter { it.length > 2 }.firstOrNull() + firstWordInLine?.let(Rarity::fromString) != null + } + if (rarityIdx >= 0) { + lore = lore.subList(0, rarityIdx + 1) + } + + trimStats() + + deleteLineUntilNextSpace { it.startsWith("Held Item: ") } + deleteLineUntilNextSpace { it.startsWith("Progress to Level ") } + deleteLineUntilNextSpace { it.startsWith("MAX LEVEL") } + deleteLineUntilNextSpace { it.startsWith("Click to view recipe!") } + collapseWhitespaces() + + name = name.transformEachRecursively { + var string = it.directLiteralStringContent ?: return@transformEachRecursively it + string = string.replace("Lvl \\d+".toRegex(), "Lvl {LVL}") + Component.literal(string).setStyle(it.style) + } + + if (lore.isEmpty()) + lore = listOf(Component.empty()) + } + + private fun trimStats() { + val lore = this.lore.toMutableList() + for (index in lore.indices) { + val value = lore[index] + val statLine = SBItemStack.parseStatLine(value) + if (statLine == null) break + val v = value.copy() + require(value.directLiteralStringContent == "") + v.siblings.removeIf { it.directLiteralStringContent!!.contains("(") } + val last = v.siblings.last() + v.siblings[v.siblings.lastIndex] = + Component.literal(last.directLiteralStringContent!!.trimEnd()) + .setStyle(last.style) + lore[index] = v + } + this.lore = lore + } + + fun collapseWhitespaces() { + lore = (listOf(null as Component?) + lore).zipWithNext() + .filter { !it.first?.unformattedString.isNullOrBlank() || !it.second?.unformattedString.isNullOrBlank() } + .map { it.second!! } + } + + fun deleteLineUntilNextSpace(search: (String) -> Boolean) { + val idx = lore.indexOfFirst { search(it.unformattedString) } + if (idx < 0) return + val l = lore.toMutableList() + val p = l.subList(idx, l.size) + val nextBlank = p.indexOfFirst { it.unformattedString.isEmpty() } + if (nextBlank < 0) + p.clear() + else + p.subList(0, nextBlank).clear() + lore = l + } + + fun processNbt() { + // TODO: calculate hideflags + legacyNbt.put("HideFlags", IntTag.valueOf(254)) + copyUnbreakable() + copyItemModel() + copyPotion() + copyExtraAttributes() + copyLegacySkullNbt() + copyDisplay() + copyColour() + copyEnchantments() + copyEnchantGlint() + // TODO: copyDisplay + } + + private fun copyPotion() { + val effects = itemStack.get(DataComponents.POTION_CONTENTS) ?: return + legacyNbt.put("CustomPotionEffects", ListTag().also { + effects.allEffects.forEach { effect -> + val effectId = effect.effect.unwrapKey().get().location().path + val duration = effect.duration + val legacyId = LegacyItemData.effectList[effectId]!! + + it.add(CompoundTag().apply { + put("Ambient", ByteTag.valueOf(false)) + put("Duration", IntTag.valueOf(duration)) + put("Id", ByteTag.valueOf(legacyId.id.toByte())) + put("Amplifier", ByteTag.valueOf(effect.amplifier.toByte())) + }) + } + }) + } + + fun CompoundTag.getOrPutCompound(name: String): CompoundTag { + val compound = getCompoundOrEmpty(name) + put(name, compound) + return compound + } + + private fun copyColour() { + if (!itemStack.`is`(ItemTags.DYEABLE)) { + itemStack.remove(DataComponents.DYED_COLOR) + return + } + val leatherTint = itemStack.componentsPatch.get(DataComponents.DYED_COLOR)?.getOrNull() ?: return + legacyNbt.getOrPutCompound("display").put("color", IntTag.valueOf(leatherTint.rgb)) + } + + private fun copyItemModel() { + val itemModel = itemStack.get(DataComponents.ITEM_MODEL) ?: return + legacyNbt.put("ItemModel", StringTag.valueOf(itemModel.toString())) + } + + private fun copyDisplay() { + legacyNbt.getOrPutCompound("display").apply { + put("Lore", lore.map { StringTag.valueOf(it.getLegacyFormatString(trimmed = true)) }.toNbtList()) + putString("Name", name.getLegacyFormatString(trimmed = true)) + } + } + + fun exportModernSnbt(): Tag { + val overlay = ItemStack.CODEC.encodeStart(MC.currentOrDefaultRegistryNbtOps, itemStack.copy().also { + it.modifyExtraAttributes { attribs -> + originalId.ifPresent { attribs.putString("id", it) } + } + }).orThrow + val overlayWithVersion = + ExportedTestConstantMeta.SOURCE_CODEC.encode(ExportedTestConstantMeta.current, NbtOps.INSTANCE, overlay) + .orThrow + return overlayWithVersion + } + + fun prepare() { + preprocess() + processNbt() + itemStack.extraAttributes = extraAttribs + } + + fun exportJson(): JsonElement { + return buildJsonObject { + val (itemId, damage) = legacyifyItemStack() + put("itemid", itemId) + put("displayname", name.getLegacyFormatString(trimmed = true)) + put("nbttag", legacyNbt.toLegacyString()) + put("damage", damage) + put("lore", lore.map { it.getLegacyFormatString(trimmed = true) }.toJsonArray()) + val sbId = itemStack.skyBlockId + if (sbId == null) + warnings.add("Could not find skyblock id") + put("internalname", sbId?.neuItem) + put("clickcommand", "") + put("crafttext", "") + put("modver", "Firmament ${Firmament.version.friendlyString}") + put("infoType", "") + put("info", JsonArray(listOf())) + } + + } + + companion object { + fun createExporter(itemStack: ItemStack): LegacyItemExporter { + return LegacyItemExporter(itemStack.copy()).also { it.prepare() } + } + + @Subscribe + fun load(event: ClientStartedEvent) { + thread(start = true, name = "ItemExporter Meta Load Thread") { + LegacyItemData.itemLut + } + } + } + + fun copyEnchantGlint() { + if (itemStack.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) == true) { + val ench = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", ench) + } + } + + private fun copyUnbreakable() { + if (itemStack.get(DataComponents.UNBREAKABLE) == Unit.INSTANCE) { + legacyNbt.putBoolean("Unbreakable", true) + } + } + + fun copyEnchantments() { + val enchantments = itemStack.get(DataComponents.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return + val enchTag = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", enchTag) + enchantments.entrySet().forEach { entry -> + val id = entry.key.unwrapKey().get().location() + val legacyId = LegacyItemData.enchantmentLut[id] + if (legacyId == null) { + warnings.add("Could not find legacy enchantment id for ${id}") + return@forEach + } + enchTag.add(CompoundTag().apply { + putShort("lvl", entry.intValue.toShort()) + putShort( + "id", + legacyId.id.toShort() + ) + }) + } + } + + fun copyExtraAttributes() { + legacyNbt.put("ExtraAttributes", extraAttribs) + } + + fun copyLegacySkullNbt() { + val profile = itemStack.get(DataComponents.PROFILE) ?: return + legacyNbt.put("SkullOwner", CompoundTag().apply { + putString("Id", profile.partialProfile().id.toString()) + putBoolean("hypixelPopulated", true) + put("Properties", CompoundTag().apply { + profile.partialProfile().properties().forEach { prop, value -> + val list = getListOrEmpty(prop) + put(prop, list) + list.add(CompoundTag().apply { + value.signature?.let { + putString("Signature", it) + } + putString("Value", value.value) + putString("Name", value.name) + }) + } + }) + }) + } + + fun legacyifyItemStack(): LegacyItemData.LegacyItemType { + // TODO: add a default here + if (itemStack.item == Items.LINGERING_POTION || itemStack.item == Items.SPLASH_POTION) + return LegacyItemData.LegacyItemType("potion", 16384) + return LegacyItemData.itemLut[itemStack.item]!! + } +} diff --git a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt index ff85c00..c12a2c6 100644 --- a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt +++ b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt @@ -1,33 +1,30 @@ package moe.nea.firmament.features.diana import kotlin.time.Duration.Companion.seconds -import net.minecraft.particle.ParticleTypes -import net.minecraft.sound.SoundEvents -import net.minecraft.util.math.Vec3d +import net.minecraft.core.particles.ParticleTypes +import net.minecraft.sounds.SoundEvents +import net.minecraft.world.phys.Vec3 import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ParticleSpawnEvent import moe.nea.firmament.events.SoundReceiveEvent import moe.nea.firmament.events.WorldKeyboardEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldRenderLastEvent -import moe.nea.firmament.events.subscription.SubscriptionOwner -import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SkyBlockIsland -import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.WarpUtil import moe.nea.firmament.util.render.RenderInWorldContext import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.SkyBlockItems -object AncestralSpadeSolver : SubscriptionOwner { +object AncestralSpadeSolver { var lastDing = TimeMark.farPast() private set private val pitches = mutableListOf<Float>() - val particlePositions = mutableListOf<Vec3d>() - var nextGuess: Vec3d? = null + val particlePositions = mutableListOf<Vec3>() + var nextGuess: Vec3? = null private set private var lastTeleportAttempt = TimeMark.farPast() @@ -35,7 +32,11 @@ object AncestralSpadeSolver : SubscriptionOwner { fun isEnabled() = DianaWaypoints.TConfig.ancestralSpadeSolver && SBData.skyblockLocation == SkyBlockIsland.HUB - && MC.player?.inventory?.containsAny { it.skyBlockId == SkyBlockItems.ANCESTRAL_SPADE } == true // TODO: add a reactive property here + && MC.player?.inventory?.hasAnyMatching { + it.skyBlockId == SkyBlockItems.ANCESTRAL_SPADE || + it.skyBlockId == SkyBlockItems.ARCHAIC_SPADE || + it.skyBlockId == SkyBlockItems.DEIFIC_SPADE + } == true // TODO: add a reactive property here @Subscribe fun onKeyBind(event: WorldKeyboardEvent) { @@ -62,7 +63,7 @@ object AncestralSpadeSolver : SubscriptionOwner { @Subscribe fun onPlaySound(event: SoundReceiveEvent) { if (!isEnabled()) return - if (!SoundEvents.BLOCK_NOTE_BLOCK_HARP.matchesId(event.sound.value().id)) return + if (!SoundEvents.NOTE_BLOCK_HARP.`is`(event.sound.value().location)) return if (lastDing.passedTime() > 1.seconds) { particlePositions.clear() @@ -96,7 +97,7 @@ object AncestralSpadeSolver : SubscriptionOwner { .let { (a, _, b) -> b.subtract(a) } .normalize() - nextGuess = event.position.add(lastParticleDirection.multiply(soundDistanceEstimate)) + nextGuess = event.position.add(lastParticleDirection.scale(soundDistanceEstimate)) } @Subscribe @@ -106,13 +107,11 @@ object AncestralSpadeSolver : SubscriptionOwner { nextGuess?.let { tinyBlock(it, 1f, 0x80FFFFFF.toInt()) // TODO: replace this - color(1f, 1f, 0f, 1f) - tracer(it, lineWidth = 3f) + tracer(it, lineWidth = 3f, color = 0x80FFFFFF.toInt()) } if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) { // TODO: replace this // TODO: add toggle - color(0f, 1f, 0f, 0.7f) - line(particlePositions) + line(particlePositions, color = 0x80FFFFFF.toInt()) } } } @@ -124,8 +123,4 @@ object AncestralSpadeSolver : SubscriptionOwner { pitches.clear() lastDing = TimeMark.farPast() } - - override val delegateFeature: FirmamentFeature - get() = DianaWaypoints - } diff --git a/src/main/kotlin/features/diana/DianaWaypoints.kt b/src/main/kotlin/features/diana/DianaWaypoints.kt index 6d87262..650e6f9 100644 --- a/src/main/kotlin/features/diana/DianaWaypoints.kt +++ b/src/main/kotlin/features/diana/DianaWaypoints.kt @@ -3,29 +3,29 @@ package moe.nea.firmament.features.diana import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.AttackBlockEvent import moe.nea.firmament.events.UseBlockEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig -object DianaWaypoints : FirmamentFeature { - override val identifier get() = "diana" - override val config get() = TConfig +object DianaWaypoints { + val identifier get() = "diana" - object TConfig : ManagedConfig(identifier, Category.EVENTS) { - val ancestralSpadeSolver by toggle("ancestral-spade") { true } - val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport") - val nearbyWaypoints by toggle("nearby-waypoints") { true } - } + @Config + object TConfig : ManagedConfig(identifier, Category.EVENTS) { + val ancestralSpadeSolver by toggle("ancestral-spade") { true } + val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport") + val nearbyWaypoints by toggle("nearby-waypoints") { true } + } - @Subscribe - fun onBlockUse(event: UseBlockEvent) { - NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos) - } + @Subscribe + fun onBlockUse(event: UseBlockEvent) { + NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos) + } - @Subscribe - fun onBlockAttack(event: AttackBlockEvent) { - NearbyBurrowsSolver.onBlockClick(event.blockPos) - } + @Subscribe + fun onBlockAttack(event: AttackBlockEvent) { + NearbyBurrowsSolver.onBlockClick(event.blockPos) + } } diff --git a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt index 2fb4002..35af660 100644 --- a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt +++ b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt @@ -2,22 +2,20 @@ package moe.nea.firmament.features.diana import me.shedaniel.math.Color import kotlin.time.Duration.Companion.seconds -import net.minecraft.particle.ParticleTypes -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.MathHelper -import net.minecraft.util.math.Position +import net.minecraft.core.particles.ParticleTypes +import net.minecraft.core.BlockPos +import net.minecraft.util.Mth +import net.minecraft.core.Position import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ParticleSpawnEvent import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldRenderLastEvent -import moe.nea.firmament.events.subscription.SubscriptionOwner -import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.collections.mutableMapWithMaxSize import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld -object NearbyBurrowsSolver : SubscriptionOwner { +object NearbyBurrowsSolver { private val recentlyDugBurrows: MutableMap<BlockPos, TimeMark> = mutableMapWithMaxSize(20) @@ -65,7 +63,7 @@ object NearbyBurrowsSolver : SubscriptionOwner { fun onParticles(event: ParticleSpawnEvent) { if (!DianaWaypoints.TConfig.nearbyWaypoints) return - val position: BlockPos = event.position.toBlockPos().down() + val position: BlockPos = event.position.toBlockPos().below() if (wasRecentlyDug(position)) return @@ -134,11 +132,8 @@ object NearbyBurrowsSolver : SubscriptionOwner { burrows.remove(blockPos) lastBlockClick = blockPos } - - override val delegateFeature: FirmamentFeature - get() = DianaWaypoints } fun Position.toBlockPos(): BlockPos { - return BlockPos(MathHelper.floor(x), MathHelper.floor(y), MathHelper.floor(z)) + return BlockPos(Mth.floor(x()), Mth.floor(y()), Mth.floor(z())) } diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt index 5151862..955d23b 100644 --- a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt +++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt @@ -1,224 +1,224 @@ - package moe.nea.firmament.features.events.anniversity import io.github.notenoughupdates.moulconfig.observer.ObservableList import io.github.notenoughupdates.moulconfig.xml.Bind -import moe.nea.jarvis.api.Point +import org.joml.Vector2i import kotlin.time.Duration.Companion.seconds -import net.minecraft.entity.passive.PigEntity -import net.minecraft.util.math.BlockPos +import net.minecraft.world.entity.animal.Pig +import net.minecraft.network.chat.Component +import net.minecraft.core.BlockPos import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.EntityInteractionEvent import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldReadyEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.gui.hud.MoulConfigHud +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.ItemNameLookup import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.MC import moe.nea.firmament.util.SHORT_NUMBER_FORMAT import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.parseShortNumber import moe.nea.firmament.util.useMatch -object AnniversaryFeatures : FirmamentFeature { - override val identifier: String - get() = "anniversary" - - object TConfig : ManagedConfig(identifier, Category.EVENTS) { - val enableShinyPigTracker by toggle("shiny-pigs") {true} - val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) } - } +object AnniversaryFeatures { + val identifier: String + get() = "anniversary" - override val config: ManagedConfig? - get() = TConfig + @Config + object TConfig : ManagedConfig(identifier, Category.EVENTS) { + val enableShinyPigTracker by toggle("shiny-pigs") { true } + val trackPigCooldown by position("pig-hud", 200, 300) { Vector2i(100, 200) } + } - data class ClickedPig( + data class ClickedPig( val clickedAt: TimeMark, val startLocation: BlockPos, - val pigEntity: PigEntity - ) { - @Bind("timeLeft") - fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration - } - - val clickedPigs = ObservableList<ClickedPig>(mutableListOf()) - var lastClickedPig: PigEntity? = null - - val pigDuration = 90.seconds - - @Subscribe - fun onTick(event: TickEvent) { - clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration } - } - - val pattern = "SHINY! You extracted (?<reward>.*) from the piglet's orb!".toPattern() - - @Subscribe - fun onChat(event: ProcessChatEvent) { - if(!TConfig.enableShinyPigTracker)return - if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") { - val pig = lastClickedPig ?: return - // TODO: store proper location based on the orb location, maybe - val startLocation = pig.blockPos ?: return - clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig)) - lastClickedPig = null - } - if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") { - val player = MC.player ?: return - val lowest = - clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return - clickedPigs.remove(lowest) - } - pattern.useMatch(event.unformattedString) { - val reward = group("reward") - val parsedReward = parseReward(reward) - addReward(parsedReward) - PigCooldown.rewards.atOnce { - PigCooldown.rewards.clear() - rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) } - } - } - } - - fun addReward(reward: Reward) { - val it = rewards.listIterator() - while (it.hasNext()) { - val merged = reward.mergeWith(it.next()) ?: continue - it.set(merged) - return - } - rewards.add(reward) - } - - val rewards = mutableListOf<Reward>() - - fun <T> ObservableList<T>.atOnce(block: () -> Unit) { - val oldObserver = observer - observer = null - block() - observer = oldObserver - update() - } - - sealed interface Reward { - fun mergeWith(other: Reward): Reward? - data class EXP(val amount: Double, val skill: String) : Reward { - override fun mergeWith(other: Reward): Reward? { - if (other is EXP && other.skill == skill) - return EXP(amount + other.amount, skill) - return null - } - } - - data class Coins(val amount: Double) : Reward { - override fun mergeWith(other: Reward): Reward? { - if (other is Coins) - return Coins(other.amount + amount) - return null - } - } - - data class Items(val amount: Int, val item: SkyblockId) : Reward { - override fun mergeWith(other: Reward): Reward? { - if (other is Items && other.item == item) - return Items(amount + other.amount, item) - return null - } - } - - data class Unknown(val text: String) : Reward { - override fun mergeWith(other: Reward): Reward? { - return null - } - } - } - - val expReward = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern() - val coinReward = "\\+(?<amount>$SHORT_NUMBER_FORMAT) coins".toPattern() - val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern() - fun parseReward(string: String): Reward { - expReward.useMatch<Unit>(string) { - val exp = parseShortNumber(group("exp")) - val kind = group("kind") - return Reward.EXP(exp, kind) - } - coinReward.useMatch<Unit>(string) { - val coins = parseShortNumber(group("amount")) - return Reward.Coins(coins) - } - itemReward.useMatch(string) { - val amount = group("amount")?.toIntOrNull() ?: 1 - val name = group("name") - val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch - return Reward.Items(amount, item) - } - return Reward.Unknown(string) - } - - @Subscribe - fun onWorldClear(event: WorldReadyEvent) { - lastClickedPig = null - clickedPigs.clear() - } - - @Subscribe - fun onEntityClick(event: EntityInteractionEvent) { - if (event.entity is PigEntity) { - lastClickedPig = event.entity - } - } - - @Subscribe - fun init(event: WorldReadyEvent) { - PigCooldown.forceInit() - } - - object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) { - override fun shouldRender(): Boolean { - return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker - } - - @Bind("pigs") - fun getPigs() = clickedPigs - - class DisplayReward(val backedBy: Reward) { - @Bind - fun count(): String { - return when (backedBy) { - is Reward.Coins -> backedBy.amount - is Reward.EXP -> backedBy.amount - is Reward.Items -> backedBy.amount - is Reward.Unknown -> 0 - }.toString() - } - - val itemStack = if (backedBy is Reward.Items) { - SBItemStack(backedBy.item, backedBy.amount) - } else { - SBItemStack(SkyblockId.NULL) - } - - @Bind - fun name(): String { - return when (backedBy) { - is Reward.Coins -> "Coins" - is Reward.EXP -> backedBy.skill - is Reward.Items -> itemStack.asImmutableItemStack().name.string - is Reward.Unknown -> backedBy.text - } - } - - @Bind - fun isKnown() = backedBy !is Reward.Unknown - } - - @get:Bind("rewards") - val rewards = ObservableList<DisplayReward>(mutableListOf()) - - } + val pigEntity: Pig + ) { + @Bind("timeLeft") + fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration + } + + val clickedPigs = ObservableList<ClickedPig>(mutableListOf()) + var lastClickedPig: Pig? = null + + val pigDuration = 90.seconds + + @Subscribe + fun onTick(event: TickEvent) { + clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration } + } + + val pattern = "SHINY! You extracted (?<reward>.*) from the piglet's orb!".toPattern() + + @Subscribe + fun onChat(event: ProcessChatEvent) { + if (!TConfig.enableShinyPigTracker) return + if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") { + val pig = lastClickedPig ?: return + // TODO: store proper location based on the orb location, maybe + val startLocation = pig.blockPosition() ?: return + clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig)) + lastClickedPig = null + } + if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") { + val player = MC.player ?: return + val lowest = + clickedPigs.minByOrNull { it.startLocation.distToCenterSqr(player.position) } ?: return + clickedPigs.remove(lowest) + } + pattern.useMatch(event.unformattedString) { + val reward = group("reward") + val parsedReward = parseReward(reward) + addReward(parsedReward) + PigCooldown.rewards.atOnce { + PigCooldown.rewards.clear() + rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) } + } + } + } + + fun addReward(reward: Reward) { + val it = rewards.listIterator() + while (it.hasNext()) { + val merged = reward.mergeWith(it.next()) ?: continue + it.set(merged) + return + } + rewards.add(reward) + } + + val rewards = mutableListOf<Reward>() + + fun <T> ObservableList<T>.atOnce(block: () -> Unit) { + val oldObserver = observer + observer = null + block() + observer = oldObserver + update() + } + + sealed interface Reward { + fun mergeWith(other: Reward): Reward? + data class EXP(val amount: Double, val skill: String) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is EXP && other.skill == skill) + return EXP(amount + other.amount, skill) + return null + } + } + + data class Coins(val amount: Double) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is Coins) + return Coins(other.amount + amount) + return null + } + } + + data class Items(val amount: Int, val item: SkyblockId) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is Items && other.item == item) + return Items(amount + other.amount, item) + return null + } + } + + data class Unknown(val text: String) : Reward { + override fun mergeWith(other: Reward): Reward? { + return null + } + } + } + + val expReward = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern() + val coinReward = "(?i)\\+(?<amount>$SHORT_NUMBER_FORMAT) Coins".toPattern() + val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern() + fun parseReward(string: String): Reward { + expReward.useMatch<Unit>(string) { + val exp = parseShortNumber(group("exp")) + val kind = group("kind") + return Reward.EXP(exp, kind) + } + coinReward.useMatch<Unit>(string) { + val coins = parseShortNumber(group("amount")) + return Reward.Coins(coins) + } + itemReward.useMatch(string) { + val amount = group("amount")?.toIntOrNull() ?: 1 + val name = group("name") + val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch + return Reward.Items(amount, item) + } + return Reward.Unknown(string) + } + + @Subscribe + fun onWorldClear(event: WorldReadyEvent) { + lastClickedPig = null + clickedPigs.clear() + } + + @Subscribe + fun onEntityClick(event: EntityInteractionEvent) { + if (event.entity is Pig) { + lastClickedPig = event.entity + } + } + + @Subscribe + fun init(event: WorldReadyEvent) { + PigCooldown.forceInit() + } + + object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) { + override fun shouldRender(): Boolean { + return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker + } + + @Bind("pigs") + fun getPigs() = clickedPigs + + class DisplayReward(val backedBy: Reward) { + @Bind + fun count(): String { + return when (backedBy) { + is Reward.Coins -> backedBy.amount + is Reward.EXP -> backedBy.amount + is Reward.Items -> backedBy.amount + is Reward.Unknown -> 0 + }.toString() + } + + val itemStack = if (backedBy is Reward.Items) { + SBItemStack(backedBy.item, backedBy.amount) + } else { + SBItemStack(SkyblockId.NULL) + } + + @OptIn(ExpensiveItemCacheApi::class) + @Bind + fun name(): Component { + return when (backedBy) { + is Reward.Coins -> Component.literal("Coins") + is Reward.EXP -> Component.literal(backedBy.skill) + is Reward.Items -> itemStack.asImmutableItemStack().hoverName + is Reward.Unknown -> Component.literal(backedBy.text) + } + } + + @Bind + fun isKnown() = backedBy !is Reward.Unknown + } + + @get:Bind("rewards") + val rewards = ObservableList<DisplayReward>(mutableListOf()) + + } } 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..9b87cc6 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.features.events.anniversity + +import java.util.Optional +import me.shedaniel.math.Color +import kotlin.jvm.optionals.getOrNull +import net.minecraft.world.entity.player.Player +import net.minecraft.network.chat.Style +import net.minecraft.ChatFormatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object CenturyRaffleFeatures { + @Config + 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, ChatFormatting.BLUE), + CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, ChatFormatting.YELLOW), + CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, ChatFormatting.GREEN), + CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, ChatFormatting.RED), + CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, ChatFormatting.LIGHT_PURPLE), + ) + + data class CakeTeam( + val id: SkyblockId, + val formatting: ChatFormatting, + ) { + val searchedTextRgb = formatting.color!! + 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? Player ?: return + val cakeColor: Style = player.feedbackDisplayName.visit( + { style, text -> + if (text == cakeIcon) Optional.of(style) + else Optional.empty() + }, Style.EMPTY).getOrNull() ?: return + if (cakeColor.color?.value == requestedCakeTeam.searchedTextRgb) { + event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay + } + } + +} diff --git a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt index 840fb8c..3f149ff 100644 --- a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt +++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt @@ -1,17 +1,16 @@ package moe.nea.firmament.features.events.carnival -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig -object CarnivalFeatures : FirmamentFeature { - object TConfig : ManagedConfig(identifier, Category.EVENTS) { +object CarnivalFeatures { + @Config + object TConfig : ManagedConfig(identifier, Category.EVENTS) { val enableBombSolver by toggle("bombs-solver") { true } val displayTutorials by toggle("tutorials") { true } } - override val config: ManagedConfig? - get() = TConfig - override val identifier: String + val identifier: String get() = "carnival" } diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt index 1824225..64b3814 100644 --- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt +++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt @@ -2,17 +2,17 @@ package moe.nea.firmament.features.events.carnival import io.github.notenoughupdates.moulconfig.observer.ObservableList -import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform import io.github.notenoughupdates.moulconfig.xml.Bind import java.util.UUID -import net.minecraft.block.Blocks -import net.minecraft.item.Item -import net.minecraft.item.ItemStack -import net.minecraft.item.Items -import net.minecraft.text.ClickEvent -import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos -import net.minecraft.world.WorldAccess +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.Component +import net.minecraft.core.BlockPos +import net.minecraft.world.level.LevelAccessor import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute import moe.nea.firmament.events.AttackBlockEvent @@ -45,7 +45,6 @@ object MinesweeperHelper { enum class Piece( - @get:Bind("fruitName") val fruitName: String, val points: Int, val specialAbility: String, @@ -118,24 +117,26 @@ object MinesweeperHelper { val textureUrl = "http://textures.minecraft.net/texture/$textureHash" val itemStack = createSkullItem(UUID.randomUUID(), textureUrl) .setSkyBlockFirmamentUiId("MINESWEEPER_$name") + @get:Bind("fruitName") + val textFruitName = Component.literal(fruitName) @Bind - fun getIcon() = ModernItemStack.of(itemStack) + fun getIcon() = MoulConfigPlatform.wrap(itemStack) - @Bind - fun pieceLabel() = fruitColor.formattingCode + fruitName + @get:Bind("pieceLabel") + val pieceLabel = Component.literal(fruitColor.formattingCode + fruitName) - @Bind - fun boardLabel() = "§a$totalPerBoard§7/§rboard" + @get:Bind("boardLabel") + val boardLabel = Component.literal("§a$totalPerBoard§7/§rboard") - @Bind("description") - fun getDescription() = buildString { + @get:Bind("description") + val getDescription = Component.literal(buildString { append(specialAbility) if (points >= 0) { append(" Default points: $points.") } } - } +) } object TutorialScreen { @get:Bind("pieces") @@ -158,7 +159,7 @@ object MinesweeperHelper { ; @Bind("itemType") - fun getItemStack() = ModernItemStack.of(ItemStack(itemType)) + fun getItemStack() = MoulConfigPlatform.wrap(ItemStack(itemType)) companion object { val id = SkyblockId("CARNIVAL_SHOVEL") @@ -175,10 +176,10 @@ object MinesweeperHelper { ) { fun toBlockPos() = BlockPos(sandBoxLow.x + x, sandBoxLow.y, sandBoxLow.z + y) - fun getBlock(world: WorldAccess) = world.getBlockState(toBlockPos()).block - fun isUnopened(world: WorldAccess) = getBlock(world) == Blocks.SAND - fun isOpened(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE - fun isScorched(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE_STAIRS + fun getBlock(world: LevelAccessor) = world.getBlockState(toBlockPos()).block + fun isUnopened(world: LevelAccessor) = getBlock(world) == Blocks.SAND + fun isOpened(world: LevelAccessor) = getBlock(world) == Blocks.SANDSTONE + fun isScorched(world: LevelAccessor) = getBlock(world) == Blocks.SANDSTONE_STAIRS companion object { fun fromBlockPos(blockPos: BlockPos): BoardPosition? { @@ -221,8 +222,8 @@ object MinesweeperHelper { @Subscribe 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")) + MC.sendChat(Component.translatable("firmament.carnival.tutorial.minesweeper").withStyle { + it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial")) }) } if (!CarnivalFeatures.TConfig.enableBombSolver) { @@ -259,7 +260,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.mainHandItem) } @Subscribe @@ -267,7 +268,7 @@ object MinesweeperHelper { val gs = gameState ?: return RenderInWorldContext.renderInWorld(event) { for ((pos, bombCount) in gs.nearbyBombs) { - this.text(pos.toBlockPos().up().toCenterPos(), Text.literal("§a$bombCount \uD83D\uDCA3")) + this.text(pos.toBlockPos().above().center, Component.literal("§a$bombCount \uD83D\uDCA3")) } } } diff --git a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt deleted file mode 100644 index 76f6ed4..0000000 --- a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt +++ /dev/null @@ -1,41 +0,0 @@ -package moe.nea.firmament.features.fixes - -import net.minecraft.particle.ParticleTypes -import net.minecraft.util.math.Vec3d -import moe.nea.firmament.annotations.Subscribe -import moe.nea.firmament.events.ParticleSpawnEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig -import moe.nea.firmament.util.compatloader.CompatLoader - -object CompatibliltyFeatures : FirmamentFeature { - override val identifier: String - get() = "compatibility" - - object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { - val enhancedExplosions by toggle("explosion-enabled") { false } - val explosionSize by integer("explosion-power", 10, 50) { 1 } - } - - override val config: ManagedConfig? - get() = TConfig - - interface ExplosiveApiWrapper { - fun spawnParticle(vec3d: Vec3d, power: Float) - - companion object : CompatLoader<ExplosiveApiWrapper>(ExplosiveApiWrapper::class.java) - } - - private val explosiveApiWrapper = ExplosiveApiWrapper.singleInstance - - @Subscribe - fun onExplosion(it: ParticleSpawnEvent) { - if (TConfig.enhancedExplosions && - it.particleEffect.type == ParticleTypes.EXPLOSION_EMITTER && - explosiveApiWrapper != null - ) { - it.cancel() - explosiveApiWrapper.spawnParticle(it.position, 2F) - } - } -} diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt index 7030319..ae4abd7 100644 --- a/src/main/kotlin/features/fixes/Fixes.kt +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -1,57 +1,72 @@ package moe.nea.firmament.features.fixes -import moe.nea.jarvis.api.Point +import org.joml.Vector2i import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable -import net.minecraft.client.MinecraftClient -import net.minecraft.client.option.KeyBinding -import net.minecraft.text.Text +import net.minecraft.client.Minecraft +import net.minecraft.client.KeyMapping +import net.minecraft.network.chat.Component 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.tr -object Fixes : FirmamentFeature { - override val identifier: String +object Fixes { + val identifier: String get() = "fixes" + @Config 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 autoSprintUnderWater by toggle("auto-sprint-underwater") { true } + var autoSprintHudToggle by toggle("auto-sprint-hud-toggle") { true } + val autoSprintHud by position("auto-sprint-hud", 80, 10) { Vector2i() } val peekChat by keyBindingWithDefaultUnbound("peek-chat") + val peekChatScroll by toggle("peek-chat-scroll") { false } val hidePotionEffects by toggle("hide-mob-effects") { false } + val hidePotionEffectsHud by toggle("hide-potion-effects-hud") { false } + val noHurtCam by toggle("disable-hurt-cam") { false } + val hideSlotHighlights by toggle("hide-slot-highlights") { false } + val hideRecipeBook by toggle("hide-recipe-book") { false } + val hideOffHand by toggle("hide-off-hand") { false } } - override val config: ManagedConfig - get() = TConfig - fun handleIsPressed( - keyBinding: KeyBinding, - cir: CallbackInfoReturnable<Boolean> + keyBinding: KeyMapping, + cir: CallbackInfoReturnable<Boolean> ) { - if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true) - cir.returnValue = true + if (keyBinding !== Minecraft.getInstance().options.keySprint) return + if (!TConfig.autoSprint) return + val player = MC.player ?: return + if (player.isSprinting) return + if (!TConfig.autoSprintUnderWater && player.isInWater) return + 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 + if (!TConfig.autoSprintKeyBinding.isBound || !TConfig.autoSprintHudToggle) return + it.context.pose().pushMatrix() + TConfig.autoSprintHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, ( + if (MC.player?.isSprinting == true) { + Component.translatable("firmament.fixes.auto-sprint.sprinting") + } else if (TConfig.autoSprint) { + if (!TConfig.autoSprintUnderWater && MC.player?.isInWater == true) + tr("firmament.fixes.auto-sprint.under-water", "In Water") + else + Component.translatable("firmament.fixes.auto-sprint.on") + } else { + Component.translatable("firmament.fixes.auto-sprint.not-sprinting") + } + ), 0, 0, -1, true ) - it.context.matrices.pop() + it.context.pose().popMatrix() } @Subscribe @@ -64,4 +79,8 @@ object Fixes : FirmamentFeature { fun shouldPeekChat(): Boolean { return TConfig.peekChat.isPressed(atLeast = true) } + + fun shouldScrollPeekedChat(): Boolean { + return TConfig.peekChatScroll + } } diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt new file mode 100644 index 0000000..edd511f --- /dev/null +++ b/src/main/kotlin/features/garden/HideComposterNoises.kt @@ -0,0 +1,34 @@ +package moe.nea.firmament.features.garden + +import net.minecraft.world.entity.animal.wolf.WolfSoundVariants +import net.minecraft.sounds.SoundEvent +import net.minecraft.sounds.SoundEvents +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig + +object HideComposterNoises { + @Config + object TConfig : ManagedConfig("composter", Category.GARDEN) { + val hideComposterNoises by toggle("no-more-noises") { false } + } + + val composterSoundEvents: List<SoundEvent> = listOf( + SoundEvents.PISTON_EXTEND, + SoundEvents.WATER_AMBIENT, + SoundEvents.CHICKEN_EGG, + SoundEvents.WOLF_SOUNDS[WolfSoundVariants.SoundSet.CLASSIC]!!.growlSound().value(), + ) + + @Subscribe + fun onNoise(event: SoundReceiveEvent) { + if (!TConfig.hideComposterNoises) return + if (SBData.skyblockLocation == SkyBlockIsland.GARDEN) { + if (event.sound.value() in composterSoundEvents) + event.cancel() + } + } +} diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt index d2c79fd..5241f54 100644 --- a/src/main/kotlin/features/inventory/CraftingOverlay.kt +++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt @@ -1,20 +1,20 @@ package moe.nea.firmament.features.inventory import io.github.moulberry.repo.data.NEUCraftingRecipe -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen -import net.minecraft.item.ItemStack -import net.minecraft.util.Formatting +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.world.item.ItemStack +import net.minecraft.ChatFormatting import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.MC import moe.nea.firmament.util.skyblockId -object CraftingOverlay : FirmamentFeature { +object CraftingOverlay { - private var screen: GenericContainerScreen? = null + private var screen: ContainerScreen? = null private var recipe: NEUCraftingRecipe? = null private var useNextScreen = false private val craftingOverlayIndices = listOf( @@ -24,7 +24,7 @@ object CraftingOverlay : FirmamentFeature { ) val CRAFTING_SCREEN_NAME = "Craft Item" - fun setOverlay(screen: GenericContainerScreen?, recipe: NEUCraftingRecipe) { + fun setOverlay(screen: ContainerScreen?, recipe: NEUCraftingRecipe) { this.screen = screen if (screen == null) { useNextScreen = true @@ -34,7 +34,7 @@ object CraftingOverlay : FirmamentFeature { @Subscribe fun onScreenChange(event: ScreenChangeEvent) { - if (useNextScreen && event.new is GenericContainerScreen + if (useNextScreen && event.new is ContainerScreen && event.new.title?.string == "Craft Item" ) { useNextScreen = false @@ -42,18 +42,19 @@ object CraftingOverlay : FirmamentFeature { } } - override val identifier: String + val identifier: String get() = "crafting-overlay" + @OptIn(ExpensiveItemCacheApi::class) @Subscribe fun onSlotRender(event: SlotRenderEvents.After) { val slot = event.slot val recipe = this.recipe ?: return - if (slot.inventory != screen?.screenHandler?.inventory) return - val recipeIndex = craftingOverlayIndices.indexOf(slot.index) + if (slot.container != screen?.menu?.container) return + val recipeIndex = craftingOverlayIndices.indexOf(slot.containerSlot) if (recipeIndex < 0) return val expectedItem = recipe.inputs[recipeIndex] - val actualStack = slot.stack ?: ItemStack.EMPTY!! + val actualStack = slot.item ?: ItemStack.EMPTY!! val actualEntry = SBItemStack(actualStack) if ((actualEntry.skyblockId != expectedItem.skyblockId || actualEntry.getStackSize() < expectedItem.amount) && expectedItem.amount.toInt() != 0 @@ -66,15 +67,15 @@ object CraftingOverlay : FirmamentFeature { 0x80FF0000.toInt() ) } - if (!slot.hasStack()) { + if (!slot.hasItem()) { val itemStack = SBItemStack(expectedItem)?.asImmutableItemStack() ?: return - event.context.drawItem(itemStack, event.slot.x, event.slot.y) - event.context.drawStackOverlay( + event.context.renderItem(itemStack, event.slot.x, event.slot.y) + event.context.renderItemDecorations( MC.font, itemStack, event.slot.x, event.slot.y, - "${Formatting.RED}${expectedItem.amount.toInt()}" + "${ChatFormatting.RED}${expectedItem.amount.toInt()}" ) } } diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt index 4aa8202..e9d0631 100644 --- a/src/main/kotlin/features/inventory/ItemHotkeys.kt +++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt @@ -2,22 +2,26 @@ package moe.nea.firmament.features.inventory import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HandledScreenKeyPressedEvent -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.HypixelStaticData -import moe.nea.firmament.repo.ItemCache import moe.nea.firmament.repo.ItemCache.asItemStack import moe.nea.firmament.repo.ItemCache.isBroken import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC +import moe.nea.firmament.util.asBazaarStock +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.focusedItemStack import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName object ItemHotkeys { + @Config object TConfig : ManagedConfig("item-hotkeys", Category.INVENTORY) { val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface") } + @OptIn(ExpensiveItemCacheApi::class) @Subscribe fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) { if (!event.matches(TConfig.openGlobalTradeInterface)) { @@ -26,7 +30,7 @@ object ItemHotkeys { var item = event.screen.focusedItemStack ?: return val skyblockId = item.skyBlockId ?: return item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item - if (HypixelStaticData.hasBazaarStock(skyblockId)) { + if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) { MC.sendCommand("bz ${item.getSearchName()}") } else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) { MC.sendCommand("ahs ${item.getSearchName()}") diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt index fdc378a..9712067 100644 --- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -1,45 +1,38 @@ package moe.nea.firmament.features.inventory import java.awt.Color -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.render.RenderLayer -import net.minecraft.item.ItemStack -import net.minecraft.util.Formatting -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HotbarItemRenderEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig -import moe.nea.firmament.util.collections.lastNotNullOfOrNull -import moe.nea.firmament.util.collections.memoizeIdentity -import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.skyblock.Rarity -import moe.nea.firmament.util.unformattedString -object ItemRarityCosmetics : FirmamentFeature { - override val identifier: String +object ItemRarityCosmetics { + val identifier: String get() = "item-rarity-cosmetics" + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val showItemRarityBackground by toggle("background") { false } val showItemRarityInHotbar by toggle("background-hotbar") { false } } - override val config: ManagedConfig - get() = TConfig - private val rarityToColor = Rarity.colourMap.mapValues { - val c = Color(it.value.colorValue!!) + val c = Color(it.value.color!!) c.rgb } - fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) { + fun drawItemStackRarity(drawContext: GuiGraphics, x: Int, y: Int, item: ItemStack) { val rarity = Rarity.fromItem(item) ?: return val rgb = rarityToColor[rarity] ?: 0xFF00FF80.toInt() - drawContext.drawGuiTexture( - RenderLayer::getGuiTextured, - Identifier.of("firmament:item_rarity_background"), + drawContext.blitSprite( + RenderPipelines.GUI_TEXTURED, + ResourceLocation.parse("firmament:item_rarity_background"), x, y, 16, 16, rgb @@ -50,7 +43,7 @@ object ItemRarityCosmetics : FirmamentFeature { @Subscribe fun onRenderSlot(it: SlotRenderEvents.Before) { if (!TConfig.showItemRarityBackground) return - val stack = it.slot.stack ?: return + val stack = it.slot.item ?: return drawItemStackRarity(it.context, it.slot.x, it.slot.y, stack) } diff --git a/src/main/kotlin/features/inventory/JunkHighlighter.kt b/src/main/kotlin/features/inventory/JunkHighlighter.kt new file mode 100644 index 0000000..15bdcfa --- /dev/null +++ b/src/main/kotlin/features/inventory/JunkHighlighter.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName +import moe.nea.firmament.util.useMatch + +object JunkHighlighter { + val identifier: String + get() = "junk-highlighter" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val junkRegex by string("regex") { "" } + val highlightBind by keyBinding("highlight") { GLFW.GLFW_KEY_LEFT_CONTROL } + } + + @Subscribe + fun onDrawSlot(event: SlotRenderEvents.After) { + if (!TConfig.highlightBind.isPressed() || TConfig.junkRegex.isEmpty()) return + val junkRegex = TConfig.junkRegex.toPattern() + val slot = event.slot + junkRegex.useMatch(slot.item.getSearchName()) { + event.context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0xffff0000.toInt()) + } + } +} diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt index 5ca10f7..e0bb4b1 100644 --- a/src/main/kotlin/features/inventory/PetFeatures.kt +++ b/src/main/kotlin/features/inventory/PetFeatures.kt @@ -1,40 +1,561 @@ package moe.nea.firmament.features.inventory -import net.minecraft.util.Identifier +import java.util.regex.Matcher +import org.joml.Vector2i +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.util.StringRepresentable +import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.ProfileSwitchEvent +import moe.nea.firmament.events.SlotClickEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.jarvis.JarvisIntegration +import moe.nea.firmament.repo.ExpLadders +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager +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.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.formattedString +import moe.nea.firmament.util.parseShortNumber 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.skyblock.TabListAPI +import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.titleCase +import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor -object PetFeatures : FirmamentFeature { - override val identifier: String +object PetFeatures { + val identifier: String get() = "pets" - override val config: ManagedConfig? - get() = TConfig - + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val highlightEquippedPet by toggle("highlight-pet") { true } + val petOverlay by toggle("pet-overlay") { false } + val petOverlayHud by position("pet-overlay-hud", 80, 10) { + Vector2i() + } + val petOverlayHudStyle by choice("pet-overlay-hud-style") { PetOverlayHudStyles.PLAIN_NO_BACKGROUND } } - val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + enum class PetOverlayHudStyles : StringRepresentable { + PLAIN_NO_BACKGROUND, + COLOUR_NO_BACKGROUND, + PLAIN_BACKGROUND, + COLOUR_BACKGROUND, + ICON_ONLY; + + override fun getSerializedName() : String { + return name + } + } + + private val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + private val autopetPattern = + "§cAutopet §eequipped your §7\\[Lvl (\\d{1,3})\\] §([fa956d])([\\w\\s]+)§e! §aVIEW RULE".toPattern() + private val petItemPattern = "§aYour pet is now holding (§[fa956d][\\w\\s]+)§a.".toPattern() + private val petLevelUpPattern = "§aYour §([fa956d])([\\w\\s]+) §aleveled up to level §9(\\d+)§a!".toPattern() + private val petMap = HashMap<String, ParsedPet>() + private var currentPetUUID: String = "" + private var tempTabPet: ParsedPet? = null + private var tempChatPet: ParsedPet? = null + + @Subscribe + fun onProfileSwitch(event: ProfileSwitchEvent) { + petMap.clear() + currentPetUUID = "" + tempTabPet = null + tempChatPet = null + } @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { - if (!TConfig.highlightEquippedPet) return - val stack = event.slot.stack - if (stack.petData?.active == true) - petMenuTitle.useMatch(MC.screenName ?: return) { + // Cache pets + petMenuTitle.useMatch(MC.screenName ?: return) { + val stack = event.slot.item + if (!stack.isEmpty) cachePet(stack) + if (stack.petData?.active == true) { + if (currentPetUUID == "") currentPetUUID = stack.skyblockUUID.toString() + // Highlight active pet feature + if (!TConfig.highlightEquippedPet) return event.context.drawGuiTexture( - event.slot.x, event.slot.y, 0, 16, 16, - Identifier.of("firmament:selected_pet_background") + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, ) } + } + } + + private fun cachePet(stack: ItemStack) { + // Cache information about a pet + if (stack.skyblockUUID == null) return + if (petMap.containsKey(stack.skyblockUUID.toString()) && + petMap[stack.skyblockUUID.toString()]?.isComplete == true) return + + val pet = PetParser.parsePetMenuSlot(stack) ?: return + petMap[stack.skyblockUUID.toString()] = pet + } + + @Subscribe + fun onSlotClick(event: SlotClickEvent) { + // Check for switching/removing pet manually + petMenuTitle.useMatch(MC.screenName ?: return) { + if (event.slot.container is Inventory) return + if (event.button != 0 && event.button != 1) return + val petData = event.stack.petData ?: return + if (petData.active == true) { + currentPetUUID = "None" + return + } + if (event.button != 0) return + if (!petMap.containsKey(event.stack.skyblockUUID.toString())) cachePet(event.stack) + currentPetUUID = event.stack.skyblockUUID.toString() + } } + @Subscribe + fun onChatEvent(event: ProcessChatEvent) { + // Handle AutoPet + var matcher = autopetPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + val tempMap = petMap.filter { (uuid, pet) -> + pet.name == matcher.group(3) && + pet.rarity == PetParser.reversePetColourMap[matcher.group(2)] && + pet.level <= matcher.group(1).toInt() + } + if (tempMap.isNotEmpty()) { + currentPetUUID = tempMap.keys.first() + } else { + tempChatPet = PetParser.parsePetChatMessage(matcher.group(3), matcher.group(2), matcher.group(1).toInt()) + currentPetUUID = "" + } + tempTabPet = null + return + } + // Handle changing pet item + // This is needed for when pet item can't be found in tab list + matcher = petItemPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + petMap[currentPetUUID]?.petItem = matcher.group(1) + tempTabPet?.petItem = matcher.group(1) + tempChatPet?.petItem = matcher.group(1) + // TODO: Handle tier boost pet items if required + // I'm not rich enough to be able to test tier boosts + } + // Handle pet levelling up + // This is needed for when pet level can't be found in tab list + matcher = petLevelUpPattern.matcher(event.text.formattedString()) + if (matcher.matches()) { + val tempPet = + PetParser.parsePetChatMessage(matcher.group(2), matcher.group(1), matcher.group(3).toInt()) ?: return + val tempMap = petMap.filter { (uuid, pet) -> + pet.name == tempPet.name && + pet.rarity == tempPet.rarity && + pet.level <= tempPet.level + } + if (tempMap.isNotEmpty()) petMap[tempMap.keys.first()]?.update(tempPet) + if (tempTabPet?.name == tempPet.name && tempTabPet?.rarity == tempPet.rarity) { + tempTabPet?.update(tempPet) + } + if (tempChatPet?.name == tempPet.name && tempChatPet?.rarity == tempPet.rarity) tempChatPet?.update(tempPet) + } + } + private fun renderLinesAndBackground(it: HudRenderEvent, lines: List<Component>) { + // Render background for the hud + if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) { + var maxWidth = 0 + lines.forEach { if (MC.font.width(it) > maxWidth) maxWidth = MC.font.width(it.unformattedString) } + val height = if (MC.font.lineHeight * lines.size > 32) MC.font.lineHeight * lines.size else 32 + it.context.fill(0, -3, 40 + maxWidth, height + 2, 0x80000000.toInt()) + } + + // Render text for the hud + lines.forEachIndexed { index, line -> + it.context.drawString( + MC.font, + line.copy().withColor(ChatFormatting.GRAY), + 36, + MC.font.lineHeight * index, + -1, + true + ) + } + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.petOverlay || !SBData.isOnSkyblock) return + + // Possibly handle Montezuma as a future feature? Could track how many pieces have been found etc + // Would likely need to be a separate config toggle though since that has + // very different usefulness/purpose to the pet hud outside of rift + if (SBData.skyblockLocation == SkyBlockIsland.RIFT) return + + // Initial data + var pet: ParsedPet? = null + // Do not render the HUD if there is no pet active + if (currentPetUUID == "None") return + // Get active pet from cache + if (currentPetUUID != "") pet = petMap[currentPetUUID] + // Parse tab widget for pet data + val tabPet = PetParser.parseTabWidget(TabListAPI.getWidgetLines(TabListAPI.WidgetName.PET)) + if (pet == null && tabPet == null && tempTabPet == null && tempChatPet == null) { + // No data on current pet + it.context.pose().pushMatrix() + TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose()) + val lines = mutableListOf<Component>() + lines.add(Component.literal("" + ChatFormatting.WHITE + "Unknown Pet")) + lines.add(Component.literal("Open Pets Menu To Fix")) + renderLinesAndBackground(it, lines) + it.context.pose().popMatrix() + return + } + if (pet == null) { + // Pet is only known through tab widget or chat message, potentially saved from tab widget elsewhere + // (e.g. another server or before removing the widget from the tab list) + pet = tabPet ?: tempTabPet ?: tempChatPet ?: return + if (tempTabPet == null) tempTabPet = tabPet + } + + // Update pet based on tab widget if needed + if (tabPet != null && pet.name == tabPet.name && pet.rarity == tabPet.rarity) { + if (tabPet.level > pet.level) { + // Level has increased since caching + pet.level = tabPet.level + pet.currentExp = tabPet.currentExp + pet.expForNextLevel = tabPet.expForNextLevel + pet.totalExp = tabPet.totalExp + } else if (tabPet.currentExp > pet.currentExp) { + // Exp has increased since caching, level has not + pet.currentExp = tabPet.currentExp + pet.totalExp = tabPet.totalExp + } + if (tabPet.petItem != pet.petItem && tabPet.petItem != "Unknown") { + // Pet item has changed since caching + pet.petItem = tabPet.petItem + pet.petItemStack = tabPet.petItemStack + } + } + + // Set the text for the HUD + + val lines = mutableListOf<Component>() + + if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_NO_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.COLOUR_BACKGROUND) { + // Colour Style + lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name) + .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE))) + + lines.add(Component.literal(pet.petItem)) + if (pet.level != pet.maxLevel) { + // Exp data + lines.add( + Component.literal( + "" + ChatFormatting.YELLOW + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}" + + ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW + + "${shortFormat(pet.expForNextLevel)} " + ChatFormatting.GOLD + + "(${formatPercent(pet.currentExp / pet.expForNextLevel)})" + ) + ) + lines.add( + Component.literal( + "" + ChatFormatting.YELLOW + "Required L100: ${shortFormat(pet.totalExp)}" + + ChatFormatting.GOLD + "/" + ChatFormatting.YELLOW + + "${shortFormat(pet.expForMax)} " + ChatFormatting.GOLD + + "(${formatPercent(pet.totalExp / pet.expForMax)})" + ) + ) + } else { + // Overflow Exp data + lines.add(Component.literal( + "" + ChatFormatting.AQUA + ChatFormatting.BOLD + "MAX LEVEL" + )) + lines.add(Component.literal( + "" + ChatFormatting.GOLD + "+" + ChatFormatting.YELLOW + "${shortFormat(pet.overflowExp)} XP" + )) + } + } else if (TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_NO_BACKGROUND || + TConfig.petOverlayHudStyle == PetOverlayHudStyles.PLAIN_BACKGROUND) { + // Plain Style + lines.add(Component.literal("[Lvl ${pet.level}] ").append(Component.literal(pet.name) + .withColor((Rarity.colourMap[pet.rarity]) ?: ChatFormatting.WHITE))) + + lines.add(Component.literal(if (pet.petItem != "None" && pet.petItem != "Unknown") + pet.petItem.substring(2) else pet.petItem)) + if (pet.level != pet.maxLevel) { + // Exp data + lines.add( + Component.literal( + "Required L${pet.level + 1}: ${shortFormat(pet.currentExp)}/" + + "${shortFormat(pet.expForNextLevel)} " + + "(${formatPercent(pet.currentExp / pet.expForNextLevel)})" + ) + ) + lines.add( + Component.literal( + "Required L100: ${shortFormat(pet.totalExp)}/${shortFormat(pet.expForMax)} " + + "(${formatPercent(pet.totalExp / pet.expForMax)})" + ) + ) + } else { + // Overflow Exp data + lines.add(Component.literal( + "MAX LEVEL" + )) + lines.add(Component.literal( + "+${shortFormat(pet.overflowExp)} XP" + )) + } + } + + // Render HUD + + it.context.pose().pushMatrix() + TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.pose()) + + renderLinesAndBackground(it, lines) + + // Draw the ItemStack + it.context.pose().pushMatrix() + it.context.pose().translate(-0.5F, -0.5F) + it.context.pose().scale(2f, 2f) + it.context.renderItem(pet.petItemStack.value, 0, 0) + it.context.pose().popMatrix() + + it.context.pose().popMatrix() + } +} + +object PetParser { + private val petNamePattern = " §7\\[Lvl (\\d{1,3})] §([fa956d])([\\w\\s]+)".toPattern() + private val petItemPattern = " (§[fa956dbc4][\\s\\w]+)".toPattern() + private val petExpPattern = " §e((?:\\d{1,3}[,.]?)+\\d*[kM]?)§6\\/§e((?:\\d{1,3}[,.]?)+\\d*[kM]?) XP §6\\(\\d+(?:.\\d+)?%\\)".toPattern() + private val petOverflowExpPattern = " §6\\+§e((?:\\d{1,3}[,.])+\\d*[kM]?) XP".toPattern() + private val katPattern = " Kat:.*".toPattern() + + val reversePetColourMap = mapOf( + "f" to Rarity.COMMON, + "a" to Rarity.UNCOMMON, + "9" to Rarity.RARE, + "5" to Rarity.EPIC, + "6" to Rarity.LEGENDARY, + "d" to Rarity.MYTHIC + ) + + val found = HashMap<String, Matcher>() + + @OptIn(ExpensiveItemCacheApi::class) + fun parsePetChatMessage(name: String, rarityCode: String, level: Int) : ParsedPet? { + val petId = name.uppercase().replace(" ", "_") + val petRarity = reversePetColourMap[rarityCode] ?: Rarity.COMMON + + val neuRarity = petRarity.neuRepoRarity ?: return null + val expLadder = ExpLadders.getExpLadder(petId, neuRarity) + + var currentExp = 0.0 + val expForNextLevel: Double + if (found.containsKey("exp")) { + currentExp = parseShortNumber(found.getValue("exp").group(1)) + expForNextLevel = parseShortNumber(found.getValue("exp").group(2)) + } else { + expForNextLevel = expLadder.getPetExpForLevel(level + 1).toDouble() - + expLadder.getPetExpForLevel(level).toDouble() + } + + val totalExpBeforeLevel = expLadder.getPetExpForLevel(level).toDouble() + val totalExp = totalExpBeforeLevel + currentExp + val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100 + val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble() + val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() } + + return ParsedPet( + name, + petRarity, + level, + -1, + expLadder, + currentExp, + expForNextLevel, + totalExp, + totalExpBeforeLevel, + expForMax, + 0.0, + "Unknown", + petItemStack, + false + ) + } + + @OptIn(ExpensiveItemCacheApi::class) + fun parseTabWidget(lines: List<Component>): ParsedPet? { + found.clear() + for (line in lines.reversed()) { + if (!found.containsKey("kat")) { + val matcher = katPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["kat"] = matcher + continue + } + } + if (!found.containsKey("exp")) { + val matcher = petExpPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["exp"] = matcher + continue + } + } + if (!found.containsKey("exp")) { + val matcher = petOverflowExpPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["overflow"] = matcher + continue + } + } + if (!found.containsKey("item")) { + val matcher = petItemPattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["item"] = matcher + continue + } + } + if (!found.containsKey("name")) { + val matcher = petNamePattern.matcher(line.formattedString()) + if (matcher.matches()) { + found["name"] = matcher + continue + } + } + } + if (!found.containsKey("name")) return null + + val petName = titleCase(found.getValue("name").group(3)) + val petRarity = reversePetColourMap.getValue(found.getValue("name").group(2)) + val petId = petName.uppercase().replace(" ", "_") + + val petLevel = found.getValue("name").group(1).toInt() + + val neuRarity = petRarity.neuRepoRarity ?: return null + val expLadder = ExpLadders.getExpLadder(petId, neuRarity) + + var currentExp = 0.0 + val expForNextLevel: Double + if (found.containsKey("exp")) { + currentExp = parseShortNumber(found.getValue("exp").group(1)) + expForNextLevel = parseShortNumber(found.getValue("exp").group(2)) + } else { + expForNextLevel = expLadder.getPetExpForLevel(petLevel + 1).toDouble() - + expLadder.getPetExpForLevel(petLevel).toDouble() + } + + val overflowExp: Double = if (found.containsKey("overflow")) + parseShortNumber(found.getValue("overflow").group(1)) else 0.0 + + val totalExpBeforeLevel = expLadder.getPetExpForLevel(petLevel).toDouble() + val totalExp = totalExpBeforeLevel + currentExp + val maxLevel = RepoManager.neuRepo.constants.petLevelingData.petLevelingBehaviourOverrides[petId]?.maxLevel ?: 100 + val expForMax = expLadder.getPetExpForLevel(maxLevel).toDouble() + val petItemStack = lazy { RepoManager.neuRepo.items.items[petId + ";" + petRarity.ordinal].asItemStack() } + + + var petItem = "Unknown" + if (found.containsKey("item")) { + petItem = found.getValue("item").group(1) + } + + return ParsedPet( + petName, + petRarity, + petLevel, + maxLevel, + expLadder, + currentExp, + expForNextLevel, + totalExp, + totalExpBeforeLevel, + expForMax, + overflowExp, + petItem, + petItemStack, + false + ) + } + + fun parsePetMenuSlot(stack: ItemStack) : ParsedPet? { + val petData = stack.petData ?: return null + val expData = petData.level + val overflow = if (expData.expTotal - expData.expRequiredForMaxLevel > 0) + (expData.expTotal - expData.expRequiredForMaxLevel).toDouble() else 0.0 + val petItem = if (stack.petData?.heldItem != null) + RepoManager.neuRepo.items.items.getValue(stack.petData?.heldItem).displayName else "None" + return ParsedPet( + titleCase(petData.type), + Rarity.fromNeuRepo(petData.tier) ?: Rarity.COMMON, + expData.currentLevel, + expData.maxLevel, + ExpLadders.getExpLadder(petData.skyblockId.toString(), petData.tier), + expData.expInCurrentLevel.toDouble(), + expData.expRequiredForNextLevel.toDouble(), + expData.expTotal.toDouble(), + expData.expTotal.toDouble() - expData.expInCurrentLevel.toDouble(), + expData.expRequiredForMaxLevel.toDouble(), + overflow, + petItem, + lazy { stack }, + true + ) + } +} + +data class ParsedPet( + val name: String, + val rarity: Rarity, + var level: Int, + val maxLevel: Int, + val expLadder: ExpLadders.ExpLadder?, + var currentExp: Double, + var expForNextLevel: Double, + var totalExp: Double, + var totalExpBeforeLevel: Double, + val expForMax: Double, + var overflowExp: Double, + var petItem: String, + var petItemStack: Lazy<ItemStack>, + var isComplete: Boolean +) { + fun update(other: ParsedPet) { + // Update the pet data to reflect another instance (of itself) + if (other.level > level) { + level = other.level + currentExp = other.currentExp + expForNextLevel = other.expForNextLevel + totalExp = other.totalExp + totalExpBeforeLevel = other.totalExpBeforeLevel + overflowExp = other.overflowExp + } else { + if (other.currentExp > currentExp) currentExp = other.currentExp + expForNextLevel = other.expForNextLevel + if (other.totalExp > totalExp) totalExp = other.totalExp + if (other.totalExpBeforeLevel > totalExpBeforeLevel) totalExpBeforeLevel = other.totalExpBeforeLevel + if (other.overflowExp > overflowExp) overflowExp = other.overflowExp + } + if (other.petItem != "Unknown") petItem = other.petItem + isComplete = false + } } diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt index 4477203..54802db 100644 --- a/src/main/kotlin/features/inventory/PriceData.kt +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -1,51 +1,120 @@ - - package moe.nea.firmament.features.inventory -import net.minecraft.text.Text +import org.lwjgl.glfw.GLFW +import net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe 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.asBazaarStock +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.getLogicalStackSize +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 { + val identifier: String + get() = "price-data" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val tooltipEnabled by toggle("enable-always") { true } + val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") + val stackSizeKey by keyBinding("stack-size-keybind") { GLFW.GLFW_KEY_LEFT_SHIFT } + val avgLowestBin by choice( + "avg-lowest-bin-days", + ) { + AvgLowestBin.THREEDAYAVGLOWESTBIN + } + } -object PriceData : FirmamentFeature { - override val identifier: String - get() = "price-data" + enum class AvgLowestBin : StringRepresentable { + OFF, + ONEDAYAVGLOWESTBIN, + THREEDAYAVGLOWESTBIN, + SEVENDAYAVGLOWESTBIN; - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val tooltipEnabled by toggle("enable-always") { true } - val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") - } + override fun getSerializedName(): String { + return name + } + } - override val config get() = TConfig + fun formatPrice(label: Component, price: Double): Component { + return Component.literal("") + .yellow() + .bold() + .append(label) + .append(": ") + .append( + Component.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) return + if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return + val sbId = it.stack.skyBlockId + val stackSize = it.stack.getLogicalStackSize() + val isShowingStack = TConfig.stackSizeKey.isPressed() + val multiplier = if (isShowingStack) stackSize else 1 + val multiplierText = + if (isShowingStack) + tr("firmament.tooltip.multiply", "Showing prices for x${stackSize}").darkGrey() + else + tr( + "firmament.tooltip.multiply.hint", + "[${TConfig.stackSizeKey.format()}] to show x${stackSize}" + ).darkGrey() + val bazaarData = HypixelStaticData.bazaarData[sbId?.asBazaarStock] + val lowestBin = HypixelStaticData.lowestBin[sbId] + val avgBinValue: Double? = when (TConfig.avgLowestBin) { + AvgLowestBin.ONEDAYAVGLOWESTBIN -> HypixelStaticData.avg1dlowestBin[sbId] + AvgLowestBin.THREEDAYAVGLOWESTBIN -> HypixelStaticData.avg3dlowestBin[sbId] + AvgLowestBin.SEVENDAYAVGLOWESTBIN -> HypixelStaticData.avg7dlowestBin[sbId] + AvgLowestBin.OFF -> null + } + if (bazaarData != null) { + it.lines.add(Component.literal("")) + it.lines.add(multiplierText) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"), + bazaarData.quickStatus.sellPrice * multiplier + ) + ) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"), + bazaarData.quickStatus.buyPrice * multiplier + ) + ) + } else if (lowestBin != null) { + it.lines.add(Component.literal("")) + it.lines.add(multiplierText) + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"), + lowestBin * multiplier + ) + ) + if (avgBinValue != null) { + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.avg-lowestbin", "AVG Lowest BIN"), + avgBinValue * multiplier + ) + ) + } + } + } } diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt index 1e9b1b8..e508016 100644 --- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt +++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt @@ -1,12 +1,13 @@ package moe.nea.firmament.features.inventory +import java.net.URI import net.fabricmc.loader.api.FabricLoader import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds import net.minecraft.SharedConstants -import net.minecraft.text.ClickEvent -import net.minecraft.text.Text +import net.minecraft.network.chat.ClickEvent +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute @@ -30,27 +31,28 @@ object REIDependencyWarner { var sentWarning = false fun modrinthLink(slug: String) = - "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name}&l=fabric" + "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getCurrentVersion().name()}&l=fabric" - fun downloadButton(modName: String, modId: String, slug: String): Text { + fun downloadButton(modName: String, modId: String, slug: String): Component { val alreadyDownloaded = FabricLoader.getInstance().isModLoaded(modId) - return Text.literal(" - ") + return Component.literal(" - ") .white() - .append(Text.literal("[").aqua()) - .append(Text.translatable("firmament.download", modName) - .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) } + .append(Component.literal("[").aqua()) + .append(Component.translatable("firmament.download", modName) + .withStyle { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) } .yellow() .also { if (alreadyDownloaded) - it.append(Text.translatable("firmament.download.already", modName) + it.append(Component.translatable("firmament.download.already", modName) .lime()) }) - .append(Text.literal("]").aqua()) + .append(Component.literal("]").aqua()) } @Subscribe fun checkREIDependency(event: SkyblockServerUpdateEvent) { if (!SBData.isOnSkyblock) return + if (!RepoManager.TConfig.warnForMissingItemListMod) return if (hasREI) return if (sentWarning) return sentWarning = true @@ -58,11 +60,11 @@ object REIDependencyWarner { delay(2.seconds) // TODO: should we offer an automatic install that actually downloads the JARs and places them into the mod folder? MC.sendChat( - Text.translatable("firmament.reiwarning").red().bold().append("\n") + Component.translatable("firmament.reiwarning").red().bold().append("\n") .append(downloadButton("RoughlyEnoughItems", reiModId, "rei")).append("\n") .append(downloadButton("Architectury API", "architectury", "architectury-api")).append("\n") .append(downloadButton("Cloth Config API", "cloth-config", "cloth-config")).append("\n") - .append(Text.translatable("firmament.reiwarning.disable") + .append(Component.translatable("firmament.reiwarning.disable") .clickCommand("/firm disablereiwarning") .grey()) ) @@ -74,9 +76,9 @@ object REIDependencyWarner { if (hasREI) return event.subcommand("disablereiwarning") { thenExecute { - RepoManager.Config.warnForMissingItemListMod = false - RepoManager.Config.save() - MC.sendChat(Text.translatable("firmament.reiwarning.disabled").yellow()) + RepoManager.TConfig.warnForMissingItemListMod = false + RepoManager.TConfig.markDirty() + MC.sendChat(Component.translatable("firmament.reiwarning.disabled").yellow()) } } } diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt index c47867b..c492a75 100644 --- a/src/main/kotlin/features/inventory/SaveCursorPosition.kt +++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt @@ -1,66 +1,63 @@ - - package moe.nea.firmament.features.inventory +import org.lwjgl.glfw.GLFW import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.milliseconds -import net.minecraft.client.util.InputUtil -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import com.mojang.blaze3d.platform.InputConstants import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.assertNotNullOr - -object SaveCursorPosition : FirmamentFeature { - override val identifier: String - get() = "save-cursor-position" - - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val enable by toggle("enable") { true } - val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } - } - - override val config: TConfig - get() = TConfig - - var savedPositionedP1: Pair<Double, Double>? = null - var savedPosition: SavedPosition? = null - - data class SavedPosition( - val middle: Pair<Double, Double>, - val cursor: Pair<Double, Double>, - val savedAt: TimeMark = TimeMark.now() - ) - - @JvmStatic - fun saveCursorOriginal(positionedX: Double, positionedY: Double) { - savedPositionedP1 = Pair(positionedX, positionedY) - } - - @JvmStatic - fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? { - if (!TConfig.enable) return null - val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance } - savedPosition = null - if (lastPosition != null && - (lastPosition.middle.first - middleX).absoluteValue < 1 && - (lastPosition.middle.second - middleY).absoluteValue < 1 - ) { - InputUtil.setCursorParameters( - MC.window.handle, - InputUtil.GLFW_CURSOR_NORMAL, - lastPosition.cursor.first, - lastPosition.cursor.second - ) - return lastPosition.cursor - } - return null - } - - @JvmStatic - fun saveCursorMiddle(middleX: Double, middleY: Double) { - if (!TConfig.enable) return - val cursorPos = assertNotNullOr(savedPositionedP1) { return } - savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos) - } +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig + +object SaveCursorPosition { + val identifier: String + get() = "save-cursor-position" + + @Config + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val enable by toggle("enable") { true } + val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } + } + + var savedPositionedP1: Pair<Double, Double>? = null + var savedPosition: SavedPosition? = null + + data class SavedPosition( + val middle: Pair<Double, Double>, + val cursor: Pair<Double, Double>, + val savedAt: TimeMark = TimeMark.now() + ) + + @JvmStatic + fun saveCursorOriginal(positionedX: Double, positionedY: Double) { + savedPositionedP1 = Pair(positionedX, positionedY) + } + + @JvmStatic + fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? { + if (!TConfig.enable) return null + val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance } + savedPosition = null + if (lastPosition != null && + (lastPosition.middle.first - middleX).absoluteValue < 1 && + (lastPosition.middle.second - middleY).absoluteValue < 1 + ) { + InputConstants.grabOrReleaseMouse( + MC.window, + InputConstants.CURSOR_NORMAL, + lastPosition.cursor.first, + lastPosition.cursor.second + ) + return lastPosition.cursor + } + return null + } + + @JvmStatic + fun saveCursorMiddle(middleX: Double, middleY: Double) { + if (!TConfig.enable) return + val cursorPos = assertNotNullOr(savedPositionedP1) { return } + savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos) + } } diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt index 99130d5..fca40c8 100644 --- a/src/main/kotlin/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -4,107 +4,197 @@ package moe.nea.firmament.features.inventory 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 -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 net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.ChestMenu +import net.minecraft.world.inventory.InventoryMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.world.inventory.ClickType +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientInitEvent import moe.nea.firmament.events.HandledScreenForegroundEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyReleasedEvent import moe.nea.firmament.events.IsSlotProtectedEvent import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.keybindings.InputModifiers import moe.nea.firmament.keybindings.SavedKeyBinding import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.CommonSoundEffects import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.accessors.castAccessor +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.json.DashlessUUIDSerializer +import moe.nea.firmament.util.lime 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.red import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.DungeonUtil +import moe.nea.firmament.util.skyblock.SkyBlockItems import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString -object SlotLocking : FirmamentFeature { - override val identifier: String +object SlotLocking { + val identifier: String get() = "slot-locking" @Serializable - data class Data( + data class DimensionData( val lockedSlots: MutableSet<Int> = mutableSetOf(), - val lockedSlotsRift: MutableSet<Int> = mutableSetOf(), + val boundSlots: BoundSlots = BoundSlots(), + ) + + @Serializable + data class Data( val lockedUUIDs: MutableSet<UUID> = mutableSetOf(), - val boundSlots: MutableMap<Int, Int> = mutableMapOf() + val rift: DimensionData = DimensionData(), + val overworld: DimensionData = DimensionData(), + ) + + + val currentWorldData + get() = if (SBData.skyblockLocation == SkyBlockIsland.RIFT) + DConfig.data.rift + else + DConfig.data.overworld + + @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)) + + } + } + } + + + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { - SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true) + SavedKeyBinding.keyWithMods(GLFW.GLFW_KEY_L, InputModifiers.of(shift = true)) } 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 slotBindOnlyInInv by toggle("bind-only-in-inv") { false } + val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option + val protectAllHuntingBoxes by toggle("hunting-box") { false } + val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } } - enum class SlotRenderLinesMode : StringIdentifiable { + enum class SlotRenderLinesMode : StringRepresentable { EVERYTHING, ONLY_BOXES, NOTHING; - override fun asString(): String { + override fun getSerializedName(): String { return name } } - override val config: TConfig - get() = TConfig - + @Config object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "locked-slots", ::Data) val lockedUUIDs get() = DConfig.data?.lockedUUIDs val lockedSlots - get() = when (SBData.skyblockLocation) { - SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift - null -> null - else -> DConfig.data?.lockedSlots - } + get() = currentWorldData?.lockedSlots - fun isSalvageScreen(screen: HandledScreen<*>?): Boolean { + fun isSalvageScreen(screen: AbstractContainerScreen<*>?): Boolean { if (screen == null) return false return screen.title.unformattedString.contains("Salvage Item") } - fun isTradeScreen(screen: HandledScreen<*>?): Boolean { + fun isTradeScreen(screen: AbstractContainerScreen<*>?): Boolean { if (screen == null) return false - val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false - if (handler.inventory.size() < 9) return false - val middlePane = handler.inventory.getStack(handler.inventory.size() - 5) + val handler = screen.menu as? ChestMenu ?: return false + if (handler.container.containerSize < 9) return false + val middlePane = handler.container.getItem(handler.container.containerSize - 5) if (middlePane == null) return false return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff" } - fun isNpcShop(screen: HandledScreen<*>?): Boolean { + fun isNpcShop(screen: AbstractContainerScreen<*>?): Boolean { if (screen == null) return false - val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false - if (handler.inventory.size() < 9) return false - val sellItem = handler.inventory.getStack(handler.inventory.size() - 5) + val handler = screen.menu as? ChestMenu ?: return false + if (handler.container.containerSize < 9) return false + val sellItem = handler.container.getItem(handler.container.containerSize - 5) if (sellItem == null) return false if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true val lore = sellItem.loreAccordingToNbt @@ -114,13 +204,19 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onSalvageProtect(event: IsSlotProtectedEvent) { if (event.slot == null) return - if (!event.slot.hasStack()) return - if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return - val inv = event.slot.inventory + if (!event.slot.hasItem()) return + if (event.slot.item.displayNameAccordingToNbt.unformattedString != "Salvage Items") return + val inv = event.slot.container var anyBlocked = false - for (i in 0 until event.slot.index) { - val stack = inv.getStack(i) - if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack)) + for (i in 0 until event.slot.containerSlot) { + val stack = inv.getItem(i) + if (IsSlotProtectedEvent.shouldBlockInteraction( + null, + ClickType.THROW, + IsSlotProtectedEvent.MoveOrigin.SALVAGE, + stack + ) + ) anyBlocked = true } if (anyBlocked) { @@ -130,46 +226,69 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onProtectUuidItems(event: IsSlotProtectedEvent) { - val doesNotDeleteItem = event.actionType == SlotActionType.SWAP - || event.actionType == SlotActionType.PICKUP - || event.actionType == SlotActionType.QUICK_MOVE - || event.actionType == SlotActionType.QUICK_CRAFT - || event.actionType == SlotActionType.CLONE - || event.actionType == SlotActionType.PICKUP_ALL + val doesNotDeleteItem = event.actionType == ClickType.SWAP + || event.actionType == ClickType.PICKUP + || event.actionType == ClickType.QUICK_MOVE + || event.actionType == ClickType.QUICK_CRAFT + || event.actionType == ClickType.CLONE + || event.actionType == ClickType.PICKUP_ALL val isSellOrTradeScreen = isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen) - if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory) + if ((!isSellOrTradeScreen || event.slot?.container !is Inventory) && doesNotDeleteItem ) return val stack = event.itemStack ?: return + if (TConfig.protectAllHuntingBoxes && (stack.isHuntingBox())) { + event.protect() + return + } val uuid = stack.skyblockUUID ?: return if (uuid in (lockedUUIDs ?: return)) { event.protect() } } + fun ItemStack.isHuntingBox(): Boolean { + return skyBlockId == SkyBlockItems.HUNTING_TOOLKIT || extraAttributes.get("tool_kit") != null + } + @Subscribe fun onProtectSlot(it: IsSlotProtectedEvent) { - if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) { + if (it.slot != null && it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf())) { it.protect() } } @Subscribe + fun onEvent(event: ClientInitEvent) { + 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 = currentWorldData?.boundSlots ?: BoundSlots() val isValidAction = - it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) + it.actionType == ClickType.QUICK_MOVE || (it.actionType == ClickType.PICKUP && !TConfig.slotBindRequireShift) if (!isValidAction) return - val handler = MC.handledScreen?.screenHandler ?: return + val handler = MC.handledScreen?.menu ?: return + if (TConfig.slotBindOnlyInInv && handler !is InventoryMenu) + 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 + if (slot != null && it.slot.container is Inventory) { + val matchingSlots = boundSlots.findMatchingSlots(slot.containerSlot) + 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) } } @@ -177,10 +296,25 @@ object SlotLocking : FirmamentFeature { fun onLockUUID(it: HandledScreenKeyPressedEvent) { if (!it.matches(TConfig.lockUUID)) return val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament ?: return - val stack = slot.stack ?: return + val stack = slot.item ?: return + if (stack.isHuntingBox()) { + MC.sendChat( + tr( + "firmament.slot-locking.hunting-box-unbindable-hint", + "The hunting box cannot be UUID bound reliably. It changes its own UUID frequently when switching tools. " + ).red().append( + tr( + "firmament.slot-locking.hunting-box-unbindable-hint.solution", + "Use the Firmament config option for locking all hunting boxes instead." + ).lime() + ) + ) + CommonSoundEffects.playFailure() + return + } val uuid = stack.skyblockUUID ?: return val lockedUUIDs = lockedUUIDs ?: return if (uuid in lockedUUIDs) { @@ -197,7 +331,7 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) { val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament val storedSlot = storedLockingSlot ?: return @@ -205,13 +339,11 @@ object SlotLocking : FirmamentFeature { storedLockingSlot = null val hotBarSlot = if (slot.isHotbar()) slot else storedSlot val invSlot = if (slot.isHotbar()) storedSlot else slot - 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 + val boundSlots = currentWorldData?.boundSlots ?: return + lockedSlots?.remove(hotBarSlot.containerSlot) + lockedSlots?.remove(invSlot.containerSlot) + boundSlots.removeDuplicateForInventory(invSlot.containerSlot) + boundSlots.insert(hotBarSlot.containerSlot, invSlot.containerSlot) DConfig.markDirty() CommonSoundEffects.playSuccess() return @@ -223,61 +355,75 @@ object SlotLocking : FirmamentFeature { } if (it.matches(TConfig.slotBind)) { storedLockingSlot = null - val boundSlots = DConfig.data?.boundSlots ?: return + val boundSlots = currentWorldData?.boundSlots ?: return if (slot != null) - boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - } + boundSlots.removeAllInvolving(slot.containerSlot) } } @Subscribe fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) { - val boundSlots = DConfig.data?.boundSlots ?: return + val boundSlots = currentWorldData?.boundSlots ?: return fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true) - val accScreen = event.screen as AccessorHandledScreen + val accScreen = event.screen.castAccessor() 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() val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot - || accScreen.focusedSlot_Firmament === inventorySlot + || 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) - event.context.drawBorder(inventorySlot.x + sx, - inventorySlot.y + sy, - 16, 16, color.color) + event.context.submitOutline( + hotbarSlot.x + sx, + hotbarSlot.y + sy, + 16, 16, color(hotbarSlot in highlitSlots).color + ) + event.context.submitOutline( // TODO: 1.21.10 + inventorySlot.x + sx, + inventorySlot.y + sy, + 16, 16, color(inventorySlot in highlitSlots).color + ) } } @Subscribe fun onRenderCurrentDraggingSlot(event: HandledScreenForegroundEvent) { val draggingSlot = storedLockingSlot ?: return - val accScreen = event.screen as AccessorHandledScreen + val accScreen = event.screen.castAccessor() val hoveredSlot = accScreen.focusedSlot_Firmament - ?.takeIf { it.inventory is PlayerInventory } + ?.takeIf { it.container is Inventory } ?.takeIf { it == draggingSlot || it.isHotbar() != draggingSlot.isHotbar() } val sx = accScreen.x_Firmament val sy = accScreen.y_Firmament val (borderX, borderY) = draggingSlot.lineCenter() - event.context.drawBorder(draggingSlot.x + sx, draggingSlot.y + sy, 16, 16, 0xFF00FF00u.toInt()) + event.context.submitOutline( + draggingSlot.x + sx, + draggingSlot.y + sy, + 16, + 16, + 0xFF00FF00u.toInt() + ) // TODO: 1.21.10 if (hoveredSlot == null) { event.context.drawLine( borderX + sx, borderY + sy, @@ -291,9 +437,11 @@ object SlotLocking : FirmamentFeature { hovX + sx, hovY + sy, me.shedaniel.math.Color.ofOpaque(0x00FF00) ) - event.context.drawBorder(hoveredSlot.x + sx, - hoveredSlot.y + sy, - 16, 16, 0xFF00FF00u.toInt()) + event.context.submitOutline( + hoveredSlot.x + sx, + hoveredSlot.y + sy, + 16, 16, 0xFF00FF00u.toInt() + ) } } @@ -301,13 +449,13 @@ object SlotLocking : FirmamentFeature { return if (isHotbar()) { x + 9 to y } else { - x + 9 to y + 17 + x + 9 to y + 16 } } fun Slot.isHotbar(): Boolean { - return index < 9 + return containerSlot < 9 } @Subscribe @@ -319,16 +467,14 @@ object SlotLocking : FirmamentFeature { fun toggleSlotLock(slot: Slot) { val lockedSlots = lockedSlots ?: return - val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf() - if (slot.inventory is PlayerInventory) { - if (boundSlots.entries.removeIf { - it.value == slot.index || it.key == slot.index - }) { + val boundSlots = currentWorldData?.boundSlots ?: BoundSlots() + if (slot.container is Inventory) { + if (boundSlots.removeAllInvolving(slot.containerSlot)) { // intentionally do nothing - } else if (slot.index in lockedSlots) { - lockedSlots.remove(slot.index) + } else if (slot.containerSlot in lockedSlots) { + lockedSlots.remove(slot.containerSlot) } else { - lockedSlots.add(slot.index) + lockedSlots.add(slot.containerSlot) } DConfig.markDirty() CommonSoundEffects.playSuccess() @@ -338,10 +484,10 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onLockSlot(it: HandledScreenKeyPressedEvent) { val inventory = MC.handledScreen ?: return - inventory as AccessorHandledScreen + inventory.castAccessor() val slot = inventory.focusedSlot_Firmament ?: return - if (slot.inventory !is PlayerInventory) return + if (slot.container !is Inventory) return if (it.matches(TConfig.slotBind)) { storedLockingSlot = storedLockingSlot ?: slot return @@ -354,17 +500,17 @@ object SlotLocking : FirmamentFeature { @Subscribe fun onRenderSlotOverlay(it: SlotRenderEvents.After) { - val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf()) - val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf()) + val isSlotLocked = it.slot.container is Inventory && it.slot.containerSlot in (lockedSlots ?: setOf()) + val isUUIDLocked = (it.slot.item?.skyblockUUID) in (lockedUUIDs ?: setOf()) if (isSlotLocked || isUUIDLocked) { - it.context.drawGuiTexture( - GuiRenderLayers.GUI_TEXTURED_NO_DEPTH, + it.context.blitSprite( + RenderPipelines.GUI_TEXTURED, when { isSlotLocked -> - (Identifier.of("firmament:slot_locked")) + (ResourceLocation.parse("firmament:slot_locked")) isUUIDLocked -> - (Identifier.of("firmament:uuid_locked")) + (ResourceLocation.parse("firmament:uuid_locked")) else -> error("unreachable") diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt index f1b77c6..9bb78c9 100644 --- a/src/main/kotlin/features/inventory/TimerInLore.kt +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -7,25 +7,29 @@ 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 net.minecraft.network.chat.Component +import net.minecraft.util.StringRepresentable 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.grey import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.timestamp import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString object TimerInLore { + @Config object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) { val showTimers by toggle("show") { true } + val showCreationTimestamp by toggle("show-creation") { true } val timerFormat by choice("format") { TimerFormat.SOCIALIST } } - enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable { + enum class TimerFormat(val formatter: DateTimeFormatter) : StringRepresentable { RFC(DateTimeFormatter.RFC_1123_DATE_TIME), LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)), SOCIALIST( @@ -45,6 +49,7 @@ object TimerInLore { appendValue(ChronoField.SECOND_OF_MINUTE, 2) }), AMERICAN("EEEE, MMM d h:mm a yyyy"), + RFCPrecise(DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss.SSS Z")), ; constructor(block: DateTimeFormatterBuilder.() -> Unit) @@ -52,7 +57,7 @@ object TimerInLore { constructor(format: String) : this(DateTimeFormatter.ofPattern(format)) - override fun asString(): String { + override fun getSerializedName(): String { return name } } @@ -80,13 +85,25 @@ 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"), + TIMEREMAININGS("Time Remaining:", "Ends at"), + COOLDOWN("Cooldown:", "Come back at"), + ONCOOLDOWN("On cooldown:", "Available 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 creationInLore(event: ItemTooltipEvent) { + if (!TConfig.showCreationTimestamp) return + val timestamp = event.stack.timestamp ?: return + val formattedTimestamp = TConfig.timerFormat.formatter.format(ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault())) + event.lines.add(tr("firmament.lore.creationtimestamp", "Created at: $formattedTimestamp").grey()) + } + + @Subscribe fun modifyLore(event: ItemTooltipEvent) { if (!TConfig.showTimers) return var lastTimer: ZonedDateTime? = null @@ -107,9 +124,13 @@ object TimerInLore { 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()) + event.lines.add( + i + 1, + tr( + "firmament.loretimer.missingrelative", + "Found a relative countdown with no baseline (Firmament)" + ).grey() + ) continue } baseLine = lastTimer @@ -119,10 +140,11 @@ object TimerInLore { 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()) + event.lines.add( + i + 1, + Component.literal("${countdownType.label}: ") + .grey() + .append(Component.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua()) ) } } diff --git a/src/main/kotlin/features/inventory/WardrobeKeybinds.kt b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt new file mode 100644 index 0000000..8d4760b --- /dev/null +++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton + +object WardrobeKeybinds { + @Config + object TConfig : ManagedConfig("wardrobe-keybinds", Category.INVENTORY) { + val wardrobeKeybinds by toggle("wardrobe-keybinds") { false } + val changePageKeybind by keyBinding("change-page") { GLFW.GLFW_KEY_ENTER } + val nextPage by keyBinding("next-page") { GLFW.GLFW_KEY_D } + val previousPage by keyBinding("previous-page") { GLFW.GLFW_KEY_A } + val slotKeybinds = (1..9).map { + keyBinding("slot-$it") { GLFW.GLFW_KEY_0 + it } + } + val allowUnequipping by toggle("allow-unequipping") { true } + } + + val slotKeybindsWithSlot = TConfig.slotKeybinds.withIndex().map { (index, keybinding) -> + index + 36 to keybinding + } + + @Subscribe + fun switchSlot(event: HandledScreenKeyPressedEvent) { + if (MC.player == null || MC.world == null || MC.interactionManager == null) return + if (event.screen !is AbstractContainerScreen<*>) return + + val regex = Regex("Wardrobe \\([12]/2\\)") + if (!regex.matches(event.screen.title.string)) return + if (!TConfig.wardrobeKeybinds) return + + if ( + event.matches(TConfig.changePageKeybind) || + event.matches(TConfig.previousPage) || + event.matches(TConfig.nextPage) + ) { + event.cancel() + + val handler = event.screen.menu + val previousSlot = handler.getSlot(45) + val nextSlot = handler.getSlot(53) + + val backPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.previousPage) + val nextPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.nextPage) + + if (backPressed && previousSlot.item.item == Items.ARROW) { + previousSlot.clickLeftMouseButton(handler) + } else if (nextPressed && nextSlot.item.item == Items.ARROW) { + nextSlot.clickLeftMouseButton(handler) + } + } + + + val slot = + slotKeybindsWithSlot + .find { event.matches(it.second.get()) } + ?.first ?: return + + event.cancel() + + val handler = event.screen.menu + val invSlot = handler.getSlot(slot) + + val itemStack = invSlot.item + val isSelected = itemStack.item == Items.LIME_DYE + val isSelectable = itemStack.item == Items.PINK_DYE + if (!isSelectable && !isSelected) return + if (!TConfig.allowUnequipping && isSelected) return + + invSlot.clickLeftMouseButton(handler) + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt index a46bd76..0cb51ca 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament.features.inventory.buttons import com.mojang.brigadier.StringReader @@ -7,80 +5,120 @@ import me.shedaniel.math.Dimension import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import kotlinx.serialization.Serializable -import net.minecraft.client.gui.DrawContext -import net.minecraft.command.CommandRegistryAccess -import net.minecraft.command.argument.ItemStackArgumentType -import net.minecraft.item.ItemStack -import net.minecraft.resource.featuretoggle.FeatureFlags -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.commands.CommandBuildContext +import net.minecraft.commands.arguments.item.ItemArgument +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.flag.FeatureFlags +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.repo.ExpensiveItemCacheApi import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.ItemCache.isBroken import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.collections.memoize +import moe.nea.firmament.util.mc.arbitraryUUID +import moe.nea.firmament.util.mc.createSkullItem import moe.nea.firmament.util.render.drawGuiTexture @Serializable data class InventoryButton( - var x: Int, - var y: Int, - var anchorRight: Boolean, - var anchorBottom: Boolean, - var icon: String? = "", - var command: String? = "", + var x: Int, + var y: Int, + var anchorRight: Boolean, + var anchorBottom: Boolean, + var icon: String? = "", + var command: String? = "", + var isGigantic: Boolean = false, ) { - companion object { - val itemStackParser by lazy { - ItemStackArgumentType.itemStack(CommandRegistryAccess.of(MC.defaultRegistries, - FeatureFlags.VANILLA_FEATURES)) - } - val dimensions = Dimension(18, 18) - val getItemForName = ::getItemForName0.memoize(1024) - fun getItemForName0(icon: String): ItemStack { - val repoItem = RepoManager.getNEUItem(SkyblockId(icon)) - var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon)) - if (repoItem == null) { - val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give")) - icon.split(" ", limit = 3).getOrNull(2) ?: icon - else icon - val componentItem = - runCatching { - itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false) - }.getOrNull() - if (componentItem != null) - itemStack = componentItem - } - return itemStack - } - } - fun render(context: DrawContext) { - context.drawGuiTexture( - 0, - 0, - 0, - dimensions.width, - dimensions.height, - Identifier.of("firmament:inventory_button_background") - ) - context.drawItem(getItem(), 1, 1) - } + val myDimension get() = if (isGigantic) bigDimension else dimensions + + companion object { + val itemStackParser by lazy { + ItemArgument.item( + CommandBuildContext.simple( + MC.defaultRegistries, + FeatureFlags.VANILLA_SET + ) + ) + } + val dimensions = Dimension(18, 18) + val gap = 2 + val bigDimension = Dimension(dimensions.width * 2 + gap, dimensions.height * 2 + gap) + val getItemForName = ::getItemForName0.memoize(1024) + + @OptIn(ExpensiveItemCacheApi::class) + fun getItemForName0(icon: String): ItemStack { + val repoItem = RepoManager.getNEUItem(SkyblockId(icon)) + var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon)) + if (repoItem == null) { + when { + icon.startsWith("skull:") -> { + itemStack = createSkullItem( + arbitraryUUID, + "https://textures.minecraft.net/texture/${icon.substring("skull:".length)}" + ) + } + + else -> { + val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give")) + icon.split(" ", limit = 3).getOrNull(2) ?: icon + else icon + val componentItem = + runCatching { + itemStackParser.parse(StringReader(giveSyntaxItem)).createItemStack(1, false) + }.getOrNull() + if (componentItem != null) + itemStack = componentItem + } + } + } + if (itemStack.item == Items.PAINTING) + ErrorUtil.logError("created broken itemstack for inventory button $icon: $itemStack") + return itemStack + } + } + + fun render(context: GuiGraphics) { + context.blitSprite( + RenderPipelines.GUI_TEXTURED, + ResourceLocation.parse("firmament:inventory_button_background"), + 0, + 0, + myDimension.width, + myDimension.height, + ) + if (isGigantic) { + context.pose().pushMatrix() + context.pose().translate(myDimension.width / 2F, myDimension.height / 2F) + context.pose().scale(2F) + context.renderItem(getItem(), -8, -8) + context.pose().popMatrix() + } else { + context.renderItem(getItem(), 1, 1) + } + } - fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank() + fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank() - fun getPosition(guiRect: Rectangle): Point { - return Point( - (if (anchorRight) guiRect.maxX else guiRect.minX) + x, - (if (anchorBottom) guiRect.maxY else guiRect.minY) + y, - ) - } + fun getPosition(guiRect: Rectangle): Point { + return Point( + (if (anchorRight) guiRect.maxX else guiRect.minX) + x, + (if (anchorBottom) guiRect.maxY else guiRect.minY) + y, + ) + } - fun getBounds(guiRect: Rectangle): Rectangle { - return Rectangle(getPosition(guiRect), dimensions) - } + fun getBounds(guiRect: Rectangle): Rectangle { + return Rectangle(getPosition(guiRect), myDimension) + } - fun getItem(): ItemStack { - return getItemForName(icon ?: "") - } + fun getItem(): ItemStack { + return getItemForName(icon ?: "") + } } diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt index ee3ae8b..6b6a2d6 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -1,17 +1,24 @@ package moe.nea.firmament.features.inventory.buttons import io.github.notenoughupdates.moulconfig.common.IItemStack -import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext 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.gui.DrawContext -import net.minecraft.client.gui.widget.ButtonWidget -import net.minecraft.client.util.InputUtil -import net.minecraft.text.Text -import net.minecraft.util.math.MathHelper -import net.minecraft.util.math.Vec2f +import net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.MultiLineTextWidget +import net.minecraft.client.gui.components.StringWidget +import net.minecraft.client.input.KeyEvent +import com.mojang.blaze3d.platform.InputConstants +import net.minecraft.network.chat.Component +import net.minecraft.util.Mth +import net.minecraft.world.phys.Vec2 import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.FragmentGuiScreen import moe.nea.firmament.util.MC @@ -28,10 +35,13 @@ class InventoryButtonEditor( @field:Bind var icon: String = originalButton.icon ?: "" + @field:Bind + var isGigantic: Boolean = originalButton.isGigantic + @Bind fun getItemIcon(): IItemStack { save() - return ModernItemStack.of(InventoryButton.getItemForName(icon)) + return MoulConfigPlatform.wrap(InventoryButton.getItemForName(icon)) } @Bind @@ -42,6 +52,7 @@ class InventoryButtonEditor( fun save() { originalButton.icon = icon + originalButton.isGigantic = isGigantic originalButton.command = command } } @@ -49,30 +60,92 @@ class InventoryButtonEditor( var buttons: MutableList<InventoryButton> = InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList() - override fun close() { + override fun onClose() { InventoryButtons.DConfig.data.buttons = buttons InventoryButtons.DConfig.markDirty() - super.close() + super.onClose() + } + + override fun resize(client: Minecraft, width: Int, height: Int) { + lastGuiRect.move( + MC.window.guiScaledWidth / 2 - lastGuiRect.width / 2, + MC.window.guiScaledHeight / 2 - lastGuiRect.height / 2 + ) + super.resize(client, width, height) } override fun init() { super.init() - addDrawableChild( - ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) { + addRenderableWidget( + MultiLineTextWidget( + lastGuiRect.minX, + 25, + Component.translatable("firmament.inventory-buttons.delete"), + MC.font + ).setCentered(true).setMaxWidth(lastGuiRect.width) + ) + addRenderableWidget( + MultiLineTextWidget( + lastGuiRect.minX, + 40, + Component.translatable("firmament.inventory-buttons.info"), + MC.font + ).setCentered(true).setMaxWidth(lastGuiRect.width) + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.reset")) { + val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==") + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 10) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.load-preset")) { val t = ClipboardUtils.getTextContents() val newButtons = InventoryButtonTemplates.loadTemplate(t) if (newButtons != null) buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) } - .position(lastGuiRect.minX + 10, lastGuiRect.minY + 35) + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 35) .width(lastGuiRect.width - 20) .build() ) - addDrawableChild( - ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) { + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.save-preset")) { ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons)) } - .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.simple-preset")) { + // Preset from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L900-L1348 + val newButtons = InventoryButtonTemplates.loadTemplate( + "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDE2MCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImJvbmVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogMTQwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiYXJtb3Jfc3RhbmRcIixcblx0XCJjb21tYW5kXCI6IFwid2FyZHJvYmVcIlxufSIsIntcblx0XCJ4XCI6IDEyMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImVuZGVyX2NoZXN0XCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDEwMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcInNrdWxsOmQ3Y2M2Njg3NDIzZDA1NzBkNTU2YWM1M2UwNjc2Y2I1NjNiYmRkOTcxN2NkODI2OWJkZWJlZDZmNmQ0ZTdiZjhcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBpc2xhbmRcIlxufSIsIntcblx0XCJ4XCI6IDgwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MzVmNGI0MGNlZjllMDE3Y2Q0MTEyZDI2YjYyNTU3ZjhjMWQ1YjE4OWRhMmU5OTUzNDIyMmJjOGNlYzdkOTE5NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Il0=" + ) + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 85) + .width(lastGuiRect.width - 20) + .build() + ) + addRenderableWidget( + Button.builder(Component.translatable("firmament.inventory-buttons.all-warps-preset")) { + // Preset from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L1817-L2276 + val newButtons = InventoryButtonTemplates.loadTemplate( + "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YzljODg4MWU0MjkxNWE5ZDI5YmI2MWExNmZiMjZkMDU5OTEzMjA0ZDI2NWRmNWI0MzliM2Q3OTJhY2Q1NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGhvbWVcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtNjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOFwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Iiwie1xuXHRcInhcIjogMixcblx0XCJ5XCI6IC00NCxcblx0XCJhbmNob3JSaWdodFwiOiB0cnVlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5YjU2ODk1Yjk2NTk4OTZhZDY0N2Y1ODU5OTIzOGFmNTMyZDQ2ZGI5YzFiMDM4OWI4YmJlYjcwOTk5ZGFiMzNkXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZHVuZ2Vvbl9odWJcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6Nzg0MGI4N2Q1MjI3MWQyYTc1NWRlZGM4Mjg3N2UwZWQzZGY2N2RjYzQyZWE0NzllYzE0NjE3NmIwMjc3OWE1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZW5kXCJcbn0iLCJ7XG5cdFwieFwiOiAxMDksXG5cdFwieVwiOiAtMTksXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IGZhbHNlLFxuXHRcImljb25cIjogXCJza3VsbDo4NmYwNmVhYTMwMDRhZWVkMDliM2Q1YjQ1ZDk3NmRlNTg0ZTY5MWMwZTljYWRlMTMzNjM1ZGU5M2QyM2I5ZWRiXCIsXG5cdFwiY29tbWFuZFwiOiBcImhvdG1cIlxufSIsIntcblx0XCJ4XCI6IDEzMCxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkVOREVSX0NIRVNUXCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDE1MSxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkJPTkVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogLTE5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkdPTERfQkxPQ0tcIixcblx0XCJjb21tYW5kXCI6IFwiYWhcIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IDIyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiR09MRF9CQVJESU5HXCIsXG5cdFwiY29tbWFuZFwiOiBcImJ6XCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjQzOGNmM2Y4ZTU0YWZjM2IzZjkxZDIwYTQ5ZjMyNGRjYTE0ODYwMDdmZTU0NTM5OTA1NTUyNGMxNzk0MWY0ZGNcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtdXNldW1cIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IC02NCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZjQ4ODBkMmMxZTdiODZlODc1MjJlMjA4ODI2NTZmNDViYWZkNDJmOTQ5MzJiMmM1ZTBkNmVjYWE0OTBjYjRjXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ2FyZGVuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtNDQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjRkM2E2YmQ5OGFjMTgzM2M2NjRjNDkwOWZmOGQyZGM2MmNlODg3YmRjZjNjYzViMzg0ODY1MWFlNWFmNmJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBiYXJuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjUxNTM5ZGRkZjllZDI1NWVjZTYzNDgxOTNjZDc1MDEyYzgyYzkzYWVjMzgxZjA1NTcyY2VjZjczNzk3MTFiM2JcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBkZXNlcnRcIlxufSIsIntcblx0XCJ4XCI6IDQsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo3M2JjOTY1ZDU3OWMzYzYwMzlmMGExN2ViN2MyZTZmYWY1MzhjN2E1ZGU4ZTYwZWM3YTcxOTM2MGQwYTg1N2E5XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ29sZFwiXG59Iiwie1xuXHRcInhcIjogMjUsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo1NjlhMWYxMTQxNTFiNDUyMTM3M2YzNGJjMTRjMjk2M2E1MDExY2RjMjVhNjU1NGM0OGM3MDhjZDk2ZWJmY1wiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGRlZXBcIlxufSIsIntcblx0XCJ4XCI6IDQ2LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MjFkYmUzMGIwMjdhY2JjZWI2MTI1NjNiZDg3N2NkN2ViYjcxOWVhNmVkMTM5OTAyN2RjZWU1OGJiOTA0OWQ0YVwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGNyeXN0YWxzXCJcbn0iLCJ7XG5cdFwieFwiOiA2Nyxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjVjYmQ5ZjVlYzFlZDAwNzI1OTk5NjQ5MWU2OWZmNjQ5YTMxMDZjZjkyMDIyN2IxYmIzYTcxZWU3YTg5ODYzZlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGZvcmdlXCJcbn0iLCJ7XG5cdFwieFwiOiA4OCxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjZiMjBiMjNjMWFhMmJlMDI3MGYwMTZiNGM5MGQ2ZWU2YjgzMzBhMTdjZmVmODc4NjlkNmFkNjBiMmZmYmYzYjVcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtaW5lc1wiXG59Iiwie1xuXHRcInhcIjogMTA5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YTIyMWY4MTNkYWNlZTBmZWY4YzU5Zjc2ODk0ZGJiMjY0MTU0NzhkOWRkZmM0NGMyZTcwOGE2ZDNiNzU0OWJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBwYXJrXCJcbn0iLCJ7XG5cdFwieFwiOiAxMzAsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5ZDdlM2IxOWFjNGYzZGVlOWM1Njc3YzEzNTMzM2I5ZDM1YTdmNTY4YjYzZDFlZjRhZGE0YjA2OGI1YTI1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgc3BpZGVyXCJcbn0iLCJ7XG5cdFwieFwiOiAxNTEsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDpjMzY4N2UyNWM2MzJiY2U4YWE2MWUwZDY0YzI0ZTY5NGMzZWVhNjI5ZWE5NDRmNGNmMzBkY2ZiNGZiY2UwNzFcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBuZXRoZXJcIlxufSJd" + ) + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .pos(lastGuiRect.minX + 10, lastGuiRect.minY + 110) .width(lastGuiRect.width - 20) .build() ) @@ -83,14 +156,20 @@ class InventoryButtonEditor( val movedButtons = mutableListOf<InventoryButton>() for (button in buttons) { if ((!button.anchorBottom && !button.anchorRight && button.x > 0 && button.y > 0)) { - MC.sendChat(tr("firmament.inventory-buttons.button-moved", - "One of your imported buttons intersects with the inventory and has been moved to the top left.")) - movedButtons.add(button.copy( - x = 0, - y = -InventoryButton.dimensions.width, - anchorRight = false, - anchorBottom = false - )) + MC.sendChat( + tr( + "firmament.inventory-buttons.button-moved", + "One of your imported buttons intersects with the inventory and has been moved to the top left." + ) + ) + movedButtons.add( + button.copy( + x = 0, + y = -InventoryButton.dimensions.width, + anchorRight = false, + anchorBottom = false + ) + ) } else { newButtons.add(button) } @@ -99,9 +178,11 @@ class InventoryButtonEditor( val zeroRect = Rectangle(0, 0, 1, 1) for (movedButton in movedButtons) { fun getPosition(button: InventoryButton, index: Int) = - button.copy(x = (index % 10) * InventoryButton.dimensions.width, - y = (index / 10) * -InventoryButton.dimensions.height, - anchorRight = false, anchorBottom = false) + button.copy( + x = (index % 10) * InventoryButton.dimensions.width, + y = (index / 10) * -InventoryButton.dimensions.height, + anchorRight = false, anchorBottom = false + ) while (true) { val newPos = getPosition(movedButton, i++) val newBounds = newPos.getBounds(zeroRect) @@ -114,35 +195,48 @@ class InventoryButtonEditor( return newButtons } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + context.pose().pushMatrix() + PanelComponent.DefaultBackgroundRenderer.VANILLA + .render( + MoulConfigRenderContext(context), + lastGuiRect.minX, lastGuiRect.minY, + lastGuiRect.width, lastGuiRect.height, + ) + context.pose().popMatrix() super.render(context, mouseX, mouseY, delta) - context.matrices.push() - context.matrices.translate(0f, 0f, -10f) - context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1) - context.matrices.pop() for (button in buttons) { val buttonPosition = button.getBounds(lastGuiRect) - context.matrices.push() - context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F) + context.pose().pushMatrix() + context.pose().translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat()) button.render(context) - context.matrices.pop() + context.pose().popMatrix() } + renderPopup(context, mouseX, mouseY, delta) } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - if (super.keyPressed(keyCode, scanCode, modifiers)) return true - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - close() + override fun keyPressed(input: KeyEvent): Boolean { + if (super.keyPressed(input)) return true + if (input.input() == GLFW.GLFW_KEY_ESCAPE) { + onClose() return true } return false } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - if (super.mouseReleased(mouseX, mouseY, button)) return true - val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) } + override fun mouseReleased(click: MouseButtonEvent): Boolean { + if (super.mouseReleased(click)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(click.x, click.y)) } if (clickedButton != null && !justPerformedAClickAction) { - createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY)) + if (InputConstants.isKeyDown( + MC.window, + InputConstants.KEY_LCONTROL + ) + ) Editor(clickedButton).delete() + else createPopup( + MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), + Point(click.x, click.y) + ) return true } justPerformedAClickAction = false @@ -150,19 +244,27 @@ class InventoryButtonEditor( return false } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { - if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + if (super.mouseDragged(click, offsetX, offsetY)) return true - if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) { - initialDragMousePosition = Vec2f(-10F, -10F) + if (initialDragMousePosition.distanceToSqr(Vec2(click.x.toFloat(), click.y.toFloat())) >= 4 * 4) { + initialDragMousePosition = Vec2(-10F, -10F) lastDraggedButton?.let { dragging -> justPerformedAClickAction = true - val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt()) + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(click.x.toInt(), click.y.toInt()) ?: return true dragging.x = offsetX dragging.y = offsetY dragging.anchorRight = anchorRight dragging.anchorBottom = anchorBottom + if (!anchorRight && offsetX > -dragging.myDimension.width + && dragging.getBounds(lastGuiRect).intersects(lastGuiRect) + ) + dragging.x = -dragging.myDimension.width + if (!anchorRight && offsetY > -dragging.myDimension.height + && dragging.getBounds(lastGuiRect).intersects(lastGuiRect) + ) + dragging.y = -dragging.myDimension.height } } return false @@ -170,7 +272,7 @@ class InventoryButtonEditor( var lastDraggedButton: InventoryButton? = null var justPerformedAClickAction = false - var initialDragMousePosition = Vec2f(-10F, -10F) + var initialDragMousePosition = Vec2(-10F, -10F) data class AnchoredCoords( val anchorRight: Boolean, @@ -180,35 +282,30 @@ class InventoryButtonEditor( ) fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? { - if (lastGuiRect.contains(mx, my) || lastGuiRect.contains( - Point( - mx + InventoryButton.dimensions.width, - my + InventoryButton.dimensions.height, - ) - ) - ) return null - val anchorRight = mx > lastGuiRect.maxX val anchorBottom = my > lastGuiRect.maxY var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX var offsetY = my - if (anchorBottom) lastGuiRect.maxY else lastGuiRect.minY - if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_SHIFT)) { - offsetX = MathHelper.floor(offsetX / 20F) * 20 - offsetY = MathHelper.floor(offsetY / 20F) * 20 + if (InputConstants.isKeyDown(MC.window, InputConstants.KEY_LSHIFT)) { + offsetX = Mth.floor(offsetX / 20F) * 20 + offsetY = Mth.floor(offsetY / 20F) * 20 } - return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + val rect = InventoryButton(offsetX, offsetY, anchorRight, anchorBottom).getBounds(lastGuiRect) + if (rect.intersects(lastGuiRect)) return null + val anchoredCoords = AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + return anchoredCoords } - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - if (super.mouseClicked(mouseX, mouseY, button)) return true - val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) } + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + if (super.mouseClicked(click, doubled)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(click.x, click.y) } if (clickedButton != null) { lastDraggedButton = clickedButton - initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat()) + initialDragMousePosition = Vec2(click.y.toFloat(), click.y.toFloat()) return true } - val mx = mouseX.toInt() - val my = mouseY.toInt() + val mx = click.x.toInt() + val my = click.y.toInt() val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null)) justPerformedAClickAction = true diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt index d282157..c6ad14d 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt @@ -1,7 +1,6 @@ package moe.nea.firmament.features.inventory.buttons -import kotlinx.serialization.encodeToString -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC @@ -18,7 +17,7 @@ object InventoryButtonTemplates { ErrorUtil.catch<InventoryButton?>("Could not import button") { Firmament.json.decodeFromString<InventoryButton>(it).also { if (it.icon?.startsWith("extra:") == true) { - MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed")) + MC.sendChat(Component.translatable("firmament.inventory-buttons.import-failed")) } } }.or { diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt index d5b5417..eaa6138 100644 --- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -1,88 +1,118 @@ - - 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.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.InventoryScreen +import net.minecraft.network.chat.Component 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.config.ManagedConfig +import moe.nea.firmament.impl.v1.FirmamentAPIImpl import moe.nea.firmament.util.MC import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.accessors.getProperRectangle +import moe.nea.firmament.util.data.Config import moe.nea.firmament.util.data.DataHolder -import moe.nea.firmament.util.accessors.getRectangle +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.gold + +object InventoryButtons { + + @Config + object TConfig : ManagedConfig("inventory-buttons-config", Category.INVENTORY) { + val _openEditor by button("open-editor") { + openEditor() + } + val hoverText by toggle("hover-text") { true } + val onlyInv by toggle("only-inv") { false } + } -object InventoryButtons : FirmamentFeature { - override val identifier: String - get() = "inventory-buttons" + @Config + object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data) - object TConfig : ManagedConfig(identifier, Category.INVENTORY) { - val _openEditor by button("open-editor") { - openEditor() - } - } + @Serializable + data class Data( + var buttons: MutableList<InventoryButton> = mutableListOf() + ) - object DConfig : DataHolder<Data>(serializer(), identifier, ::Data) + fun getValidButtons(screen: AbstractContainerScreen<*>): Sequence<InventoryButton> { + if (TConfig.onlyInv && screen !is InventoryScreen) return emptySequence() + if (FirmamentAPIImpl.extensions.any { it.shouldHideInventoryButtons(screen) }) { + return emptySequence() + } + return DConfig.data.buttons.asSequence().filter(InventoryButton::isValid) + } - @Serializable - data class Data( - var buttons: MutableList<InventoryButton> = mutableListOf() - ) + @Subscribe + fun onRectangles(it: HandledScreenPushREIEvent) { + val bounds = it.screen.getProperRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.block(buttonBounds) + } + } - override val config: ManagedConfig - get() = TConfig + @Subscribe + fun onClickScreen(it: HandledScreenClickEvent) { + val bounds = it.screen.getProperRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + if (buttonBounds.contains(it.mouseX, it.mouseY)) { + MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */) + break + } + } + } - fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() } + var lastHoveredComponent: InventoryButton? = null + var lastMouseMove = TimeMark.farPast() - @Subscribe - fun onRectangles(it: HandledScreenPushREIEvent) { - val bounds = it.screen.getRectangle() - for (button in getValidButtons()) { - val buttonBounds = button.getBounds(bounds) - it.block(buttonBounds) - } - } + @Subscribe + fun onRenderForeground(it: HandledScreenForegroundEvent) { + val bounds = it.screen.getProperRectangle() - @Subscribe - fun onClickScreen(it: HandledScreenClickEvent) { - val bounds = it.screen.getRectangle() - for (button in getValidButtons()) { - val buttonBounds = button.getBounds(bounds) - if (buttonBounds.contains(it.mouseX, it.mouseY)) { - MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */) - break - } - } - } + var hoveredComponent: InventoryButton? = null + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.context.pose().pushMatrix() + it.context.pose().translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat()) + button.render(it.context) + it.context.pose().popMatrix() - @Subscribe - fun onRenderForeground(it: HandledScreenForegroundEvent) { - val bounds = it.screen.getRectangle() - 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() - } - lastRectangle = bounds - } + if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) { + hoveredComponent = button + if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) { + it.context.setComponentTooltipForNextFrame( + MC.font, + listOf(Component.literal(button.command).gold()), + buttonBounds.minX - 15, + buttonBounds.maxY + 20, + ) + } + } + } + if (hoveredComponent !== lastHoveredComponent) + lastMouseMove = TimeMark.now() + lastHoveredComponent = hoveredComponent + lastRectangle = bounds + } - var lastRectangle: Rectangle? = null - fun openEditor() { - ScreenUtil.setScreenLater( - InventoryButtonEditor( - lastRectangle ?: Rectangle( - MC.window.scaledWidth / 2 - 100, - MC.window.scaledHeight / 2 - 100, - 200, 200, - ) - ) - ) - } + var lastRectangle: Rectangle? = null + fun openEditor() { + ScreenUtil.setScreenLater( + InventoryButtonEditor( + lastRectangle ?: Rectangle( + MC.window.guiScaledWidth / 2 - 88, + MC.window.guiScaledHeight / 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..964f415 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -4,9 +4,9 @@ package moe.nea.firmament.features.inventory.storageoverlay import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -import net.minecraft.client.gui.screen.Screen -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen -import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.world.inventory.ChestMenu import moe.nea.firmament.util.ifMatches import moe.nea.firmament.util.unformattedString @@ -16,24 +16,24 @@ import moe.nea.firmament.util.unformattedString sealed interface StorageBackingHandle { sealed interface HasBackingScreen { - val handler: GenericContainerScreenHandler + val handler: ChestMenu } /** * The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage` */ - data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen + data class Overview(override val handler: ChestMenu) : StorageBackingHandle, HasBackingScreen /** * An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via * the [Overview] or via `/ec <index + 1>` for enderchest pages. */ - data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) : + data class Page(override val handler: ChestMenu, val storagePageSlot: StoragePageSlot) : 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 @@ -46,13 +46,13 @@ sealed interface StorageBackingHandle { returnsNotNull() implies (screen != null) } if (screen == null) return null - if (screen !is GenericContainerScreen) return null + if (screen !is ContainerScreen) return null val title = screen.title.unformattedString - if (title == "Storage") return Overview(screen.screenHandler) + if (title == "Storage") return Overview(screen.menu) return title.ifMatches(enderChestName) { - Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) + Page(screen.menu, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) } ?: title.ifMatches(backPackName) { - Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) + Page(screen.menu, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) } } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt index 2e807de..7f96637 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -1,59 +1,106 @@ package moe.nea.firmament.features.inventory.storageoverlay +import io.github.notenoughupdates.moulconfig.ChromaColour import java.util.SortedMap import kotlinx.serialization.serializer -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen -import net.minecraft.client.gui.screen.ingame.HandledScreen -import net.minecraft.entity.player.PlayerInventory -import net.minecraft.item.Items -import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.Items +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ChestInventoryUpdateEvent import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotClickEvent +import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.events.TickEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.MC +import moe.nea.firmament.util.async.discard import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder -object StorageOverlay : FirmamentFeature { - +object StorageOverlay { + @Config object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData) - override val identifier: String + val identifier: String get() = "storage-overlay" + @Config object TConfig : ManagedConfig(identifier, Category.INVENTORY) { val alwaysReplace by toggle("always-replace") { true } + val outlineActiveStoragePage by toggle("outline-active-page") { false } + val outlineActiveStoragePageColour by colour("outline-active-page-colour") { + ChromaColour.fromRGB( + 255, + 255, + 0, + 0, + 255 + ) + } + val showInactivePageTooltips by toggle("inactive-page-tooltips") { false } val columns by integer("rows", 1, 10) { 3 } val height by integer("height", 80, 3000) { 3 * 18 * 6 } + val retainScroll by toggle("retain-scroll") { true } val scrollSpeed by integer("scroll-speed", 1, 50) { 10 } 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 } + val highlightSearchResults by toggle("highlight-search-results") { true } + val highlightSearchResultsColour by colour("highlight-search-results-colour") { + ChromaColour.fromRGB( + 0, + 176, + 0, + 0, + 255 + ) + } + } + + @Subscribe + fun highlightSlots(event: SlotRenderEvents.Before) { + if (!TConfig.highlightSearchResults) return + val storageOverlayScreen = + (MC.screen as? StorageOverlayScreen) + ?: (MC.handledScreen?.customGui as? StorageOverlayCustom)?.overview + ?: return + val stack = event.slot.item ?: return + val search = storageOverlayScreen.searchText.get().takeIf { it.isNotBlank() } ?: return + if (storageOverlayScreen.matchesSearch(stack, search)) { + event.context.fill( + event.slot.x, + event.slot.y, + event.slot.x + 16, + event.slot.y + 16, + TConfig.highlightSearchResultsColour.getEffectiveColourRGB() + ) + } } + fun adjustScrollSpeed(amount: Double): Double { return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1) } - override val config: TConfig - get() = TConfig - var lastStorageOverlay: StorageOverviewScreen? = null var skipNextStorageOverlayBackflip = false var currentHandler: StorageBackingHandle? = null @Subscribe - fun onTick(event: TickEvent) { - rememberContent(currentHandler ?: return) + fun onChestContentUpdate(event: ChestInventoryUpdateEvent) { + rememberContent(currentHandler) } @Subscribe fun onClick(event: SlotClickEvent) { - if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9 + if (lastStorageOverlay != null && event.slot.container !is Inventory && event.slot.containerSlot < 9 && event.stack.item != Items.BLACK_STAINED_GLASS_PANE ) { skipNextStorageOverlayBackflip = true @@ -64,18 +111,18 @@ object StorageOverlay : FirmamentFeature { fun onScreenChange(it: ScreenChangeEvent) { if (it.old == null && it.new == null) return val storageOverlayScreen = it.old as? StorageOverlayScreen - ?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview + ?: ((it.old as? AbstractContainerScreen<*>)?.customGui as? StorageOverlayCustom)?.overview var storageOverviewScreen = it.old as? StorageOverviewScreen - val screen = it.new as? GenericContainerScreen + val screen = it.new as? ContainerScreen + rememberContent(currentHandler) val oldHandler = currentHandler currentHandler = StorageBackingHandle.fromScreen(screen) - rememberContent(currentHandler) if (storageOverviewScreen != null && oldHandler is StorageBackingHandle.HasBackingScreen) { val player = MC.player assert(player != null) - player?.networkHandler?.sendPacket(CloseHandledScreenC2SPacket(oldHandler.handler.syncId)) - if (player?.currentScreenHandler === oldHandler.handler) { - player.currentScreenHandler = player.playerScreenHandler + player?.connection?.send(ServerboundContainerClosePacket(oldHandler.handler.containerId)) + if (player?.containerMenu === oldHandler.handler) { + player.containerMenu = player.inventoryMenu } } storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay @@ -100,25 +147,24 @@ object StorageOverlay : FirmamentFeature { screen.customGui = StorageOverlayCustom( currentHandler ?: return, screen, - storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)) + storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return) + ) } fun rememberContent(handler: StorageBackingHandle?) { handler ?: return - // TODO: Make all of these functions work on deltas / updates instead of the entire contents - val data = Data.data?.storageInventories ?: return + val data = Data.data.storageInventories when (handler) { is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data) is StorageBackingHandle.Page -> rememberPage(handler, data) } - Data.markDirty() } private fun rememberStorageOverview( handler: StorageBackingHandle.Overview, data: SortedMap<StoragePageSlot, StorageData.StorageInventory> ) { - for ((index, stack) in handler.handler.stacks.withIndex()) { + for ((index, stack) in handler.handler.items.withIndex()) { // TODO: replace with slot iteration // Ignore unloaded item stacks if (stack.isEmpty) continue val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue @@ -132,15 +178,15 @@ object StorageOverlay : FirmamentFeature { data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null) } } + Data.markDirty() } private fun rememberPage( handler: StorageBackingHandle.Page, data: SortedMap<StoragePageSlot, StorageData.StorageInventory> ) { - // TODO: FIXME: FIXME NOW: Definitely don't copy all of this every tick into persistence val newStacks = - VirtualInventory(handler.handler.stacks.take(handler.handler.rows * 9).drop(9).map { it.copy() }) + VirtualInventory(handler.handler.items.take(handler.handler.rowCount * 9).drop(9).map { it.copy() }) data.compute(handler.storagePageSlot) { slot, existingInventory -> (existingInventory ?: StorageData.StorageInventory( slot.defaultName(), @@ -150,5 +196,6 @@ object StorageOverlay : FirmamentFeature { it.inventory = newStacks } } + Data.markDirty(newStacks.serializationCache.discard()) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt index 6092e26..98e8085 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -2,21 +2,27 @@ package moe.nea.firmament.features.inventory.storageoverlay import me.shedaniel.math.Point import me.shedaniel.math.Rectangle -import net.minecraft.client.MinecraftClient -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen -import net.minecraft.entity.player.PlayerInventory -import net.minecraft.screen.slot.Slot +import net.minecraft.client.Minecraft +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.ContainerScreen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.inventory.Slot import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.accessors.castAccessor import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.focusedItemStack class StorageOverlayCustom( - val handler: StorageBackingHandle, - val screen: GenericContainerScreen, - val overview: StorageOverlayScreen, + val handler: StorageBackingHandle, + val screen: ContainerScreen, + val overview: StorageOverlayScreen, ) : CustomGui() { override fun onVoluntaryExit(): Boolean { overview.isExiting = true + StorageOverlayScreen.resetScroll() return super.onVoluntaryExit() } @@ -24,20 +30,20 @@ class StorageOverlayCustom( return overview.getBounds() } - override fun afterSlotRender(context: DrawContext, slot: Slot) { - if (slot.inventory !is PlayerInventory) + override fun afterSlotRender(context: GuiGraphics, slot: Slot) { + if (slot.container !is Inventory) context.disableScissor() } - override fun beforeSlotRender(context: DrawContext, slot: Slot) { - if (slot.inventory !is PlayerInventory) + override fun beforeSlotRender(context: GuiGraphics, slot: Slot) { + if (slot.container !is Inventory) overview.createScissors(context) } override fun onInit() { - overview.init(MinecraftClient.getInstance(), screen.width, screen.height) + overview.init(Minecraft.getInstance(), screen.width, screen.height) overview.init() - screen as AccessorHandledScreen + screen.castAccessor() screen.x_Firmament = overview.measurements.x screen.y_Firmament = overview.measurements.y screen.backgroundWidth_Firmament = overview.measurements.totalWidth @@ -47,7 +53,7 @@ class StorageOverlayCustom( override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean { if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY)) return false - if (slot.inventory !is PlayerInventory) { + if (slot.container !is Inventory) { if (!overview.getScrollPanelInner().contains(pointX, pointY)) return false } @@ -58,48 +64,50 @@ class StorageOverlayCustom( return false } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - return overview.mouseReleased(mouseX, mouseY, button) + override fun mouseReleased(click: MouseButtonEvent): Boolean { + return overview.mouseReleased(click) } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { - return overview.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { + return overview.mouseDragged(click, offsetX, offsetY) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return overview.keyReleased(keyCode, scanCode, modifiers) + override fun keyReleased(input: KeyEvent): Boolean { + return overview.keyReleased(input) } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - return overview.keyPressed(keyCode, scanCode, modifiers) + override fun keyPressed(input: KeyEvent): Boolean { + return overview.keyPressed(input) } - override fun charTyped(chr: Char, modifiers: Int): Boolean { - return overview.charTyped(chr, modifiers) + override fun charTyped(input: CharacterEvent): Boolean { + return overview.charTyped(input) } - override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { - return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot) + override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean { + return overview.mouseClicked(click, doubled, (handler as? StorageBackingHandle.Page)?.storagePageSlot) } - override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + override fun render(drawContext: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) { overview.drawBackgrounds(drawContext) - overview.drawPages(drawContext, - mouseX, - mouseY, - delta, - (handler as? StorageBackingHandle.Page)?.storagePageSlot, - screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9), - Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament)) + overview.drawPages( + drawContext, + mouseX, + mouseY, + delta, + (handler as? StorageBackingHandle.Page)?.storagePageSlot, + screen.menu.slots.take(screen.menu.rowCount * 9).drop(9), + Point((screen.castAccessor()).x_Firmament, screen.y_Firmament) + ) overview.drawScrollBar(drawContext) overview.drawControls(drawContext, mouseX, mouseY) } override fun moveSlot(slot: Slot) { - val index = slot.index + val index = slot.containerSlot if (index in 0..<36) { val (x, y) = overview.getPlayerInventorySlotPosition(index) - slot.x = x - (screen as AccessorHandledScreen).x_Firmament + slot.x = x - (screen.castAccessor()).x_Firmament slot.y = y - screen.y_Firmament } else { slot.x = -100000 @@ -113,6 +121,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 cf1cf1d..d56e4d2 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -13,13 +13,17 @@ import io.github.notenoughupdates.moulconfig.observer.Property import java.util.TreeSet import me.shedaniel.math.Point import me.shedaniel.math.Rectangle -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.Screen -import net.minecraft.client.gui.screen.ingame.HandledScreen -import net.minecraft.item.ItemStack -import net.minecraft.screen.slot.Slot -import net.minecraft.text.Text -import net.minecraft.util.Identifier +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.input.CharacterEvent +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.item.ItemStack +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component +import net.minecraft.ChatFormatting +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.gui.EmptyComponent import moe.nea.firmament.gui.FirmButtonComponent @@ -35,10 +39,11 @@ 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 -class StorageOverlayScreen : Screen(Text.literal("")) { +class StorageOverlayScreen : Screen(Component.literal("")) { companion object { val PLAYER_WIDTH = 184 @@ -46,19 +51,28 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val PLAYER_Y_INSET = 3 val SLOT_SIZE = 18 val PADDING = 10 - val PAGE_WIDTH = SLOT_SIZE * 9 + val PAGE_SLOTS_WIDTH = SLOT_SIZE * 9 + val PAGE_WIDTH = PAGE_SLOTS_WIDTH + 4 val HOTBAR_X = 12 val HOTBAR_Y = 67 val MAIN_INVENTORY_Y = 9 val SCROLL_BAR_WIDTH = 8 val SCROLL_BAR_HEIGHT = 16 + val CONTROL_X_INSET = 3 + val CONTROL_Y_INSET = 5 val CONTROL_WIDTH = 70 - val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + PLAYER_Y_INSET - val CONTROL_HEIGHT = 100 + val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + CONTROL_X_INSET + 1 + val CONTROL_HEIGHT = 50 + + var scroll: Float = 0F + var lastRenderedInnerHeight = 0 + + fun resetScroll() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0F + } } var isExiting: Boolean = false - var scroll: Float = 0F var pageWidthCount = StorageOverlay.TConfig.columns inner class Measurements { @@ -67,20 +81,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val x = width / 2 - overviewWidth / 2 val overviewHeight = minOf( height - PLAYER_HEIGHT - minOf(80, height / 10), - StorageOverlay.TConfig.height) + StorageOverlay.TConfig.height + ) val innerScrollPanelHeight = overviewHeight - PADDING * 2 val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2 val playerX = width / 2 - PLAYER_WIDTH / 2 val playerY = y + overviewHeight - PLAYER_Y_INSET - val controlX = x - CONTROL_WIDTH - val controlY = y + overviewHeight / 2 - CONTROL_HEIGHT / 2 + val controlX = playerX - CONTROL_WIDTH + CONTROL_X_INSET + val controlY = playerY - CONTROL_Y_INSET val totalWidth = overviewWidth val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT } var measurements = Measurements() - var lastRenderedInnerHeight = 0 public override fun init() { super.init() pageWidthCount = StorageOverlay.TConfig.columns @@ -99,6 +113,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat()) return true } + fun coerceScroll(offset: Float) { scroll = (scroll + offset) .coerceAtMost(getMaxScroll()) @@ -107,19 +122,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) { fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height - val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory") - val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background") - val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row") - val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background") - val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob") - val controllerBackground = Identifier.of("firmament:storageoverlay/storage_controls") + val playerInventorySprite = ResourceLocation.parse("firmament:storageoverlay/player_inventory") + val upperBackgroundSprite = ResourceLocation.parse("firmament:storageoverlay/upper_background") + val slotRowSprite = ResourceLocation.parse("firmament:storageoverlay/storage_row") + val scrollbarBackground = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_background") + val scrollbarKnob = ResourceLocation.parse("firmament:storageoverlay/scroll_bar_knob") + val controllerBackground = ResourceLocation.parse("firmament:storageoverlay/storage_controls") - override fun close() { + override fun onClose() { isExiting = true - super.close() + resetScroll() + super.onClose() } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) drawBackgrounds(context) drawPages(context, mouseX, mouseY, delta, null, null, Point()) @@ -132,7 +148,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { return scroll / getMaxScroll() } - fun drawScrollBar(context: DrawContext) { + fun drawScrollBar(context: GuiGraphics) { val sbRect = getScrollBarRect() context.drawGuiTexture( scrollbarBackground, @@ -148,21 +164,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) { fun editPages() { isExiting = true - val hs = MC.screen as? HandledScreen<*> - if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) { - hs.customGui = null - } else { - MC.sendCommand("storage") + MC.instance.schedule { + val hs = MC.screen as? AbstractContainerScreen<*> + if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) { + hs.customGui = null + hs.init(MC.instance, width, height) + } else { + MC.sendCommand("storage") + } } } val guiContext = GuiContext(EmptyComponent()) private val knobStub = EmptyComponent() - val editButton = FirmButtonComponent(TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), action = ::editPages) + val editButton = FirmButtonComponent( + TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), + action = ::editPages + ) val searchText = Property.of("") // TODO: sync with REI - val searchField = TextFieldComponent(searchText, 100, GetSetter.constant(true), - tr("firmament.storage-overlay.search.suggestion", "Search...").string, - IMinecraft.instance.defaultFontRenderer) + val searchField = TextFieldComponent( + searchText, 100, GetSetter.constant(true), + tr("firmament.storage-overlay.search.suggestion", "Search...").string, + IMinecraft.INSTANCE.defaultFontRenderer + ) val controlComponent = PanelComponent( ColumnComponent( searchField, @@ -180,30 +204,36 @@ class StorageOverlayScreen : Screen(Text.literal("")) { guiContext.adopt(controlComponent) } - fun drawControls(context: DrawContext, mouseX: Int, mouseY: Int) { + fun drawControls(context: GuiGraphics, mouseX: Int, mouseY: Int) { context.drawGuiTexture( controllerBackground, measurements.controlX, measurements.controlY, - CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT) + CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT + ) context.drawMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX, mouseY) + mouseX, mouseY + ) } - fun drawBackgrounds(context: DrawContext) { - context.drawGuiTexture(upperBackgroundSprite, - measurements.x, - measurements.y, - measurements.overviewWidth, - measurements.overviewHeight) - context.drawGuiTexture(playerInventorySprite, - measurements.playerX, - measurements.playerY, - PLAYER_WIDTH, - PLAYER_HEIGHT) + fun drawBackgrounds(context: GuiGraphics) { + context.drawGuiTexture( + upperBackgroundSprite, + measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight + ) + context.drawGuiTexture( + playerInventorySprite, + measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT + ) } fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> { @@ -216,55 +246,63 @@ class StorageOverlayScreen : Screen(Text.literal("")) { ) } - fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - val items = MC.player?.inventory?.main ?: return + fun drawPlayerInventory(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { + val items = MC.player?.inventory?.nonEquipmentItems ?: return items.withIndex().forEach { (index, item) -> val (x, y) = getPlayerInventorySlotPosition(index) - context.drawItem(item, x, y, 0) - context.drawStackOverlay(textRenderer, item, x, y) + context.renderItem(item, x, y, 0) + context.renderItemDecorations(font, item, x, y) } } fun getScrollBarRect(): Rectangle { - return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING, - measurements.y + PADDING, - SCROLL_BAR_WIDTH, - measurements.innerScrollPanelHeight) + return Rectangle( + measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING, + measurements.y + PADDING, + SCROLL_BAR_WIDTH, + measurements.innerScrollPanelHeight + ) } fun getScrollPanelInner(): Rectangle { - return Rectangle(measurements.x + PADDING, - measurements.y + PADDING, - measurements.innerScrollPanelWidth, - measurements.innerScrollPanelHeight) + return Rectangle( + measurements.x + PADDING, + measurements.y + PADDING, + measurements.innerScrollPanelWidth, + measurements.innerScrollPanelHeight + ) } - fun createScissors(context: DrawContext) { + fun createScissors(context: GuiGraphics) { 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(), ) } fun drawPages( - context: DrawContext, mouseX: Int, mouseY: Int, delta: Float, - excluding: StoragePageSlot?, - slots: List<Slot>?, - slotOffset: Point + context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float, + excluding: StoragePageSlot?, + slots: List<Slot>?, + slotOffset: Point ) { createScissors(context) val data = StorageOverlay.Data.data ?: StorageData() layoutedForEach(data) { rect, page, inventory -> - drawPage(context, - rect.x, - rect.y, - page, inventory, - if (excluding == page) slots else null, - slotOffset + drawPage( + context, + rect.x, + rect.y, + page, inventory, + if (excluding == page) slots else null, + slotOffset, + mouseX, + mouseY ) } context.disableScissor() + } @@ -272,40 +310,45 @@ class StorageOverlayScreen : Screen(Text.literal("")) { get() = guiContext.focusedElement == knobStub set(value) = knobStub.setFocus(value) - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - return mouseClicked(mouseX, mouseY, button, null) + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { + return mouseClicked(click, doubled, null) } - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseReleased(click: MouseButtonEvent): Boolean { if (knobGrabbed) { knobGrabbed = false return true } - if (clickMCComponentInPlace(controlComponent, - measurements.controlX, measurements.controlY, - CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX.toInt(), mouseY.toInt(), - MouseEvent.Click(button, false)) + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + click.x.toInt(), click.y.toInt(), + MouseEvent.Click(click.button(), false) + ) ) return true - return super.mouseReleased(mouseX, mouseY, button) + return super.mouseReleased(click) } - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + override fun mouseDragged(click: MouseButtonEvent, offsetX: Double, offsetY: Double): Boolean { if (knobGrabbed) { val sbRect = getScrollBarRect() - val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight() + val percentage = (click.x - sbRect.getY()) / sbRect.getHeight() scroll = (getMaxScroll() * percentage).toFloat() mouseScrolled(0.0, 0.0, 0.0, 0.0) return true } - return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + return super.mouseDragged(click, offsetX, offsetY) } - fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + fun mouseClicked(click: MouseButtonEvent, doubled: Boolean, 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. + val mouseX = click.x + val mouseY = click.y if (getScrollPanelInner().contains(mouseX, mouseY)) { - val data = StorageOverlay.Data.data ?: StorageData() + val data = StorageOverlay.Data.data layoutedForEach(data) { rect, page, _ -> - if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) { + if (rect.contains(mouseX, mouseY) && activePage != page && click.button() == 0) { page.navigateTo() return true } @@ -320,52 +363,58 @@ class StorageOverlayScreen : Screen(Text.literal("")) { knobGrabbed = true return true } - if (clickMCComponentInPlace(controlComponent, - measurements.controlX, measurements.controlY, - CONTROL_WIDTH, CONTROL_HEIGHT, - mouseX.toInt(), mouseY.toInt(), - MouseEvent.Click(button, true)) + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + mouseX.toInt(), mouseY.toInt(), + MouseEvent.Click(click.button(), true) + ) ) return true return false } - override fun charTyped(chr: Char, modifiers: Int): Boolean { + override fun charTyped(input: CharacterEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.CharTyped(chr) + KeyboardEvent.CharTyped(input.codepointAsString().first()) // TODO: i dont like this .first() ) ) { return true } - return super.charTyped(chr, modifiers) + return super.charTyped(input) } - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + override fun keyReleased(input: KeyEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.KeyPressed(keyCode, false) + KeyboardEvent.KeyPressed(input.input(), input.scancode, false) ) ) { return true } - return super.keyReleased(keyCode, scanCode, modifiers) + return super.keyReleased(input) + } + + 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 { + override fun keyPressed(input: KeyEvent): Boolean { if (typeMCComponentInPlace( controlComponent, measurements.controlX, measurements.controlY, CONTROL_WIDTH, CONTROL_HEIGHT, - KeyboardEvent.KeyPressed(keyCode, true) + KeyboardEvent.KeyPressed(input.input(), input.scancode, true) ) ) { return true } - return super.keyPressed(keyCode, scanCode, modifiers) + return super.keyPressed(input) } @@ -414,7 +463,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) { val filter = getFilteredPages() for ((page, inventory) in data.storageInventories.entries) { if (page !in filter) continue - val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight } + val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + font.lineHeight } ?: 18 maxHeight = maxOf(maxHeight, currentHeight) val rect = Rectangle( @@ -435,61 +484,102 @@ class StorageOverlayScreen : Screen(Text.literal("")) { } fun drawPage( - context: DrawContext, - x: Int, - y: Int, - page: StoragePageSlot, - inventory: StorageData.StorageInventory, - slots: List<Slot>?, - slotOffset: Point, + context: GuiGraphics, + x: Int, + y: Int, + page: StoragePageSlot, + inventory: StorageData.StorageInventory, + slots: List<Slot>?, + slotOffset: Point, + mouseX: Int, + mouseY: Int, ): Int { val inv = inventory.inventory if (inv == null) { context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18) - context.drawText(textRenderer, - Text.literal("TODO: open this page"), - x + 4, - y + 4, - -1, - true) + context.drawString( + font, + Component.literal("TODO: open this page"), + x + 4, + y + 4, + -1, + true + ) return 18 } assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 } - val name = page.defaultName() - context.drawText(textRenderer, Text.literal(name), x + 4, y + 2, - if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true) - context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE) + val name = inventory.title + val pageHeight = inv.rows * SLOT_SIZE + 8 + font.lineHeight + if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage) + context.submitOutline( + x, + y + 3 + font.lineHeight, + PAGE_WIDTH, + inv.rows * SLOT_SIZE + 4, + StorageOverlay.TConfig.outlineActiveStoragePageColour.getEffectiveColourRGB() + ) + context.drawString( + font, Component.literal(name), x + 6, y + 3, + if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true + ) + context.drawGuiTexture( + slotRowSprite, + x + 2, + y + 5 + font.lineHeight, + PAGE_SLOTS_WIDTH, + inv.rows * SLOT_SIZE + ) + val scrollPanel = getScrollPanelInner() inv.stacks.forEachIndexed { index, stack -> - val slotX = (index % 9) * SLOT_SIZE + x + 1 - val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1 - val fakeSlot = FakeSlot(stack, slotX, slotY) + val slotX = (index % 9) * SLOT_SIZE + x + 3 + val slotY = (index / 9) * SLOT_SIZE + y + 5 + font.lineHeight + 1 if (slots == null) { + val fakeSlot = FakeSlot(stack, slotX, slotY) SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot)) - context.drawItem(stack, slotX, slotY) - context.drawStackOverlay(textRenderer, stack, slotX, slotY) + context.renderItem(stack, slotX, slotY) + context.renderItemDecorations(font, stack, slotX, slotY) SlotRenderEvents.After.publish(SlotRenderEvents.After(context, fakeSlot)) + if (StorageOverlay.TConfig.showInactivePageTooltips && !stack.isEmpty && + mouseX >= slotX && mouseY >= slotY && + mouseX <= slotX + 16 && mouseY <= slotY + 16 && + scrollPanel.contains(mouseX, mouseY)) { + try { + context.setTooltipForNextFrame(font, stack, mouseX, mouseY) + } catch (e: IllegalStateException) { + context.setComponentTooltipForNextFrame(font, listOf(Component.nullToEmpty(ChatFormatting.RED.toString() + + "Error Getting Tooltip!"), Component.nullToEmpty(ChatFormatting.YELLOW.toString() + + "Open page to fix" + ChatFormatting.RESET)), mouseX, mouseY) + } + } } else { val slot = slots[index] slot.x = slotX - slotOffset.x slot.y = slotY - slotOffset.y } } - return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight + return pageHeight + 6 } fun getBounds(): List<Rectangle> { return listOf( - Rectangle(measurements.x, - measurements.y, - measurements.overviewWidth, - measurements.overviewHeight), - Rectangle(measurements.playerX, - measurements.playerY, - PLAYER_WIDTH, - PLAYER_HEIGHT), - Rectangle(measurements.controlX, - measurements.controlY, - CONTROL_WIDTH, - CONTROL_HEIGHT)) + Rectangle( + measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight + ), + Rectangle( + measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT + ), + Rectangle( + measurements.controlX, + measurements.controlY, + CONTROL_WIDTH, + CONTROL_HEIGHT + ) + ) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt index 9112fab..3c40fc6 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt @@ -4,17 +4,19 @@ package moe.nea.firmament.features.inventory.storageoverlay import org.lwjgl.glfw.GLFW import kotlin.math.max -import net.minecraft.block.Blocks -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.Screen -import net.minecraft.item.Item -import net.minecraft.item.Items -import net.minecraft.text.Text -import net.minecraft.util.DyeColor +import net.minecraft.world.level.block.Blocks +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.input.KeyEvent +import net.minecraft.world.item.Item +import net.minecraft.world.item.Items +import net.minecraft.network.chat.Component +import net.minecraft.world.item.DyeColor import moe.nea.firmament.util.MC import moe.nea.firmament.util.toShedaniel -class StorageOverviewScreen() : Screen(Text.empty()) { +class StorageOverviewScreen() : Screen(Component.empty()) { companion object { val emptyStorageSlotItems = listOf<Item>( Blocks.RED_STAINED_GLASS_PANE.asItem(), @@ -22,39 +24,49 @@ class StorageOverviewScreen() : Screen(Text.empty()) { Items.GRAY_DYE ) val pageWidth get() = 19 * 9 + + var scroll = 0 + var lastRenderedHeight = 0 } val content = StorageOverlay.Data.data ?: StorageData() var isClosing = false - var scroll = 0 - var lastRenderedHeight = 0 + override fun init() { + super.init() + scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0) + } + + override fun onClose() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0 + super.onClose() + } - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + override fun render(context: GuiGraphics, mouseX: Int, mouseY: Int, delta: Float) { super.render(context, mouseX, mouseY, delta) context.fill(0, 0, width, height, 0x90000000.toInt()) layoutedForEach { (key, value), offsetX, offsetY -> - context.matrices.push() - context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F) + context.pose().pushMatrix() + context.pose().translate(offsetX.toFloat(), offsetY.toFloat()) renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY) - context.matrices.pop() + context.pose().popMatrix() } } inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) { var offsetY = 0 - var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll + var currentMaxHeight = StorageOverlay.TConfig.margin - StorageOverlay.TConfig.padding - scroll var totalHeight = -currentMaxHeight content.storageInventories.onEachIndexed { index, (key, value) -> - val pageX = (index % StorageOverlay.config.columns) + val pageX = (index % StorageOverlay.TConfig.columns) if (pageX == 0) { - currentMaxHeight += StorageOverlay.config.padding + currentMaxHeight += StorageOverlay.TConfig.padding offsetY += currentMaxHeight totalHeight += currentMaxHeight currentMaxHeight = 0 } val xPosition = - width / 2 - (StorageOverlay.config.columns * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding) + width / 2 - (StorageOverlay.TConfig.columns * (pageWidth + StorageOverlay.TConfig.padding) - StorageOverlay.TConfig.padding) / 2 + pageX * (pageWidth + StorageOverlay.TConfig.padding) onEach(Pair(key, value), xPosition, offsetY) val height = getStorePageHeight(value) currentMaxHeight = max(currentMaxHeight, height) @@ -62,22 +74,22 @@ class StorageOverviewScreen() : Screen(Text.empty()) { lastRenderedHeight = totalHeight + currentMaxHeight } - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseClicked(click: MouseButtonEvent, doubled: Boolean): Boolean { layoutedForEach { (k, p), x, y -> - val rx = mouseX - x - val ry = mouseY - y + val rx = click.x - x + val ry = click.y - y if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) { - close() + onClose() StorageOverlay.lastStorageOverlay = this k.navigateTo() return true } } - return super.mouseClicked(mouseX, mouseY, button) + return super.mouseClicked(click, doubled) } fun getStorePageHeight(page: StorageData.StorageInventory): Int { - return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60 + return page.inventory?.rows?.let { it * 19 + MC.font.lineHeight + 2 } ?: 60 } override fun mouseScrolled( @@ -88,36 +100,38 @@ class StorageOverviewScreen() : Screen(Text.empty()) { ): Boolean { scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt() - .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0) + .coerceAtMost(getMaxScroll()).coerceAtLeast(0) return true } - private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { - context.drawText(MC.font, page.title, 2, 2, -1, true) + private fun getMaxScroll() = lastRenderedHeight - height + 2 * StorageOverlay.TConfig.margin + + private fun renderStoragePage(context: GuiGraphics, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { + context.drawString(MC.font, page.title, 2, 2, -1, true) val inventory = page.inventory if (inventory == null) { // TODO: Missing texture context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color) - context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1) + context.drawCenteredString(MC.font, Component.literal("Not loaded yet"), pageWidth / 2, 30, -1) return } for ((index, stack) in inventory.stacks.withIndex()) { val x = (index % 9) * 19 - val y = (index / 9) * 19 + MC.font.fontHeight + 2 + val y = (index / 9) * 19 + MC.font.lineHeight + 2 if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) { context.fill(x, y, x + 18, y + 18, 0x80808080.toInt()) } else { context.fill(x, y, x + 18, y + 18, 0x40808080.toInt()) } - context.drawItem(stack, x + 1, y + 1) - context.drawStackOverlay(MC.font, stack, x + 1, y + 1) + context.renderItem(stack, x + 1, y + 1) + context.renderItemDecorations(MC.font, stack, x + 1, y + 1) } } - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - if (keyCode == GLFW.GLFW_KEY_ESCAPE) + override fun keyPressed(input: KeyEvent): Boolean { + if (input.input() == GLFW.GLFW_KEY_ESCAPE) isClosing = true - return super.keyPressed(keyCode, scanCode, modifiers) + return super.keyPressed(input) } } diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index 3b86184..69d686f 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -1,9 +1,10 @@ package moe.nea.firmament.features.inventory.storageoverlay -import io.ktor.util.decodeBase64Bytes -import io.ktor.util.encodeBase64 import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.util.Base64 +import java.util.concurrent.CompletableFuture +import kotlinx.coroutines.async import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -11,13 +12,16 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import net.minecraft.item.ItemStack -import net.minecraft.nbt.NbtCompound +import kotlin.jvm.optionals.getOrNull +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtIo -import net.minecraft.nbt.NbtList +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtSizeTracker -import net.minecraft.registry.RegistryOps +import net.minecraft.nbt.NbtAccounter +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.inventory.storageoverlay.VirtualInventory.Serializer.writeToByteArray +import moe.nea.firmament.util.Base64Util import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.mc.TolerantRegistriesOps @@ -28,6 +32,10 @@ data class VirtualInventory( ) { val rows = stacks.size / 9 + val serializationCache = CompletableFuture.supplyAsync { + writeToByteArray(this) + } + init { assert(stacks.size % 9 == 0) assert(stacks.size / 9 in 1..5) @@ -35,41 +43,47 @@ data class VirtualInventory( object Serializer : KSerializer<VirtualInventory> { + fun writeToByteArray(value: VirtualInventory): ByteArray { + val list = ListTag() + val ops = getOps() + value.stacks.forEach { + if (it.isEmpty) list.add(CompoundTag()) + else list.add(ErrorUtil.catch("Could not serialize item") { + ItemStack.CODEC.encode( + it, + ops, + CompoundTag() + ).orThrow + } + .or { CompoundTag() }) + } + val baos = ByteArrayOutputStream() + NbtIo.writeCompressed(CompoundTag().also { it.put(INVENTORY, list) }, baos) + return baos.toByteArray() + } + 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 n = NbtIo.readCompressed(ByteArrayInputStream(Base64Util.decodeBytes(s)), NbtAccounter.create(100_000_000)) + val items = n.getList(INVENTORY).getOrNull() val ops = getOps() - return VirtualInventory(items.map { - it as NbtCompound + return VirtualInventory(items?.map { + it as CompoundTag 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) + fun getOps() = MC.currentOrDefaultRegistryNbtOps 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()) + encoder.encodeString(Base64Util.encodeToString(value.serializationCache.get())) } } } diff --git a/src/main/kotlin/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt new file mode 100644 index 0000000..cc58f8a --- /dev/null +++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt @@ -0,0 +1,139 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.util.LinkedList +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.core.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object BlockZapperOverlay { + val identifier: String + get() = "block-zapper-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var blockZapperOverlay by toggle("block-zapper-overlay") { false } + val color by colour("color") { ChromaColour.fromStaticRGB(160, 0, 0, 60) } + var undoKey by keyBindingWithDefaultUnbound("undo-key") + } + + val bannedZapper: List<Block> = listOf<Block>( + Blocks.WHEAT, + Blocks.CARROTS, + Blocks.POTATOES, + Blocks.PUMPKIN, + Blocks.PUMPKIN_STEM, + Blocks.MELON, + Blocks.MELON_STEM, + Blocks.CACTUS, + Blocks.SUGAR_CANE, + Blocks.NETHER_WART, + Blocks.TALL_GRASS, + Blocks.SUNFLOWER, + Blocks.FARMLAND, + Blocks.BREWING_STAND, + Blocks.SNOW, + Blocks.RED_MUSHROOM, + Blocks.BROWN_MUSHROOM, + ) + + private val zapperOffsets: List<BlockPos> = listOf( + BlockPos(0, 0, -1), + BlockPos(0, 0, 1), + BlockPos(-1, 0, 0), + BlockPos(1, 0, 0), + BlockPos(0, 1, 0), + BlockPos(0, -1, 0) + ) + + // Skidded from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CustomItemEffects.java#L1281-L1355 (Modified) + @Subscribe + fun renderBlockZapperOverlay(event: WorldRenderLastEvent) { + if (!TConfig.blockZapperOverlay) return + val player = MC.player ?: return + val world = player.level ?: return + val heldItem = MC.stackInHand + if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + val hitResult = MC.instance.hitResult ?: return + + val zapperBlocks: HashSet<BlockPos> = HashSet() + val returnablePositions = LinkedList<BlockPos>() + + if (hitResult is BlockHitResult && hitResult.type == HitResult.Type.BLOCK) { + var pos: BlockPos = hitResult.blockPos + val firstBlockState: BlockState = world.getBlockState(pos) + val block = firstBlockState.block + + val initialAboveBlock = world.getBlockState(pos.above()).block + if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) { + var i = 0 + while (i < 164) { + zapperBlocks.add(pos) + returnablePositions.remove(pos) + + val availableNeighbors: MutableList<BlockPos> = ArrayList() + + for (offset in zapperOffsets) { + val newPos = pos.offset(offset) + + if (zapperBlocks.contains(newPos)) continue + + val state: BlockState? = world.getBlockState(newPos) + if (state != null && state.block === block) { + val above = newPos.above() + val aboveBlock = world.getBlockState(above).block + if (!bannedZapper.contains(aboveBlock)) { + availableNeighbors.add(newPos) + } + } + } + + if (availableNeighbors.size >= 2) { + returnablePositions.add(pos) + pos = availableNeighbors[0] + } else if (availableNeighbors.size == 1) { + pos = availableNeighbors[0] + } else if (returnablePositions.isEmpty()) { + break + } else { + i-- + pos = returnablePositions.last() + } + + i++ + } + } + + RenderInWorldContext.renderInWorld(event) { + if (MC.player?.isShiftKeyDown ?: false) { + zapperBlocks.forEach { + block(it, TConfig.color.getEffectiveColourRGB()) + } + } else { + sharedVoxelSurface(zapperBlocks, TConfig.color.getEffectiveColourRGB()) + } + } + } + } + + @Subscribe + fun onWorldKeyboard(it: WorldKeyboardEvent) { + if (!TConfig.undoKey.isBound) return + if (!it.matches(TConfig.undoKey)) return + if (MC.stackInHand.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + MC.sendCommand("undozap") + } +} diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt new file mode 100644 index 0000000..3f16922 --- /dev/null +++ b/src/main/kotlin/features/items/BonemerangOverlay.kt @@ -0,0 +1,94 @@ +package moe.nea.firmament.features.items + +import me.shedaniel.math.Color +import org.joml.Vector2i +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.entity.player.Player +import net.minecraft.ChatFormatting +import net.minecraft.world.phys.AABB +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr + +object BonemerangOverlay { + val identifier: String + get() = "bonemerang-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var bonemerangOverlay by toggle("bonemerang-overlay") { false } + val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Vector2i() } + var highlightHitEntities by toggle("highlight-hit-entities") { false } + } + + fun getEntities(): MutableSet<LivingEntity> { + val entities = mutableSetOf<LivingEntity>() + val camera = MC.camera as? Player ?: return entities + val player = MC.player ?: return entities + val world = player.level ?: return entities + + val cameraPos = camera.eyePosition + val rayDirection = camera.lookAngle.normalize() + val endPos = cameraPos.add(rayDirection.scale(15.0)) + val foundEntities = world.getEntities(camera, AABB(cameraPos, endPos).inflate(1.0)) + + for (entity in foundEntities) { + if (entity !is LivingEntity || entity is ArmorStand || entity.isInvisible) continue + val hitResult = entity.boundingBox.inflate(0.35).clip(cameraPos, endPos).orElse(null) + if (hitResult != null) entities.add(entity) + } + + return entities + } + + + val throwableWeapons = listOf( + SkyBlockItems.BONE_BOOMERANG, SkyBlockItems.STARRED_BONE_BOOMERANG, + SkyBlockItems.TRIBAL_SPEAR, + ) + + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightHitEntities) return + if (MC.stackInHand.skyBlockId !in throwableWeapons) return + + val entities = getEntities() + if (entities.isEmpty()) return + if (event.entity !in entities) return + + val tintOverlay by lazy { + TintedOverlayTexture().setColor(Color.ofOpaque(ChatFormatting.BLUE.color!!)) + } + + event.renderState.overlayTexture_firmament = tintOverlay + } + + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.bonemerangOverlay) return + if (MC.stackInHand.skyBlockId !in throwableWeapons) return + + val entities = getEntities() + + it.context.pose().pushMatrix() + TConfig.bonemerangOverlayHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, String.format( + tr( + "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s" + ).string, entities.size + ), 0, 0, -1, true + ) + it.context.pose().popMatrix() + } +} diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt new file mode 100644 index 0000000..a59fcbd --- /dev/null +++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt @@ -0,0 +1,234 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import net.minecraft.world.level.block.Blocks +import net.minecraft.core.Holder +import net.minecraft.tags.BlockTags +import net.minecraft.tags.TagKey +import net.minecraft.network.chat.Component +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.Vec3 +import net.minecraft.world.phys.shapes.Shapes +import net.minecraft.world.level.BlockGetter +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr + +object EtherwarpOverlay { + val identifier: String + get() = "etherwarp-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var etherwarpOverlay by toggle("etherwarp-overlay") { false } + var onlyShowWhileSneaking by toggle("only-show-while-sneaking") { true } + var cube by toggle("cube") { true } + val cubeColour by colour("cube-colour") { ChromaColour.fromStaticRGB(172, 0, 255, 60) } + val failureCubeColour by colour("cube-colour-fail") { ChromaColour.fromStaticRGB(255, 0, 172, 60) } + val tooCloseCubeColour by colour("cube-colour-tooclose") { ChromaColour.fromStaticRGB(0, 255, 0, 60) } + val tooFarCubeColour by colour("cube-colour-toofar") { ChromaColour.fromStaticRGB(255, 255, 0, 60) } + var wireframe by toggle("wireframe") { false } + var failureText by toggle("failure-text") { false } + } + + enum class EtherwarpResult(val label: Component?, val color: () -> ChromaColour) { + SUCCESS(null, TConfig::cubeColour), + INTERACTION_BLOCKED( + tr("firmament.etherwarp.fail.tooclosetointeractable", "Too close to interactable"), + TConfig::tooCloseCubeColour + ), + TOO_DISTANT(tr("firmament.etherwarp.fail.toofar", "Too far away"), TConfig::tooFarCubeColour), + OCCUPIED(tr("firmament.etherwarp.fail.occupied", "Occupied"), TConfig::failureCubeColour), + } + + val interactionBlocked = Checker( + setOf( + Blocks.HOPPER, + Blocks.CHEST, + Blocks.ENDER_CHEST, + Blocks.FURNACE, + Blocks.CRAFTING_TABLE, + Blocks.CAULDRON, + Blocks.WATER_CAULDRON, + Blocks.ENCHANTING_TABLE, + Blocks.DISPENSER, + Blocks.DROPPER, + Blocks.BREWING_STAND, + Blocks.TRAPPED_CHEST, + Blocks.LEVER, + ), + setOf( + BlockTags.DOORS, + BlockTags.TRAPDOORS, + BlockTags.ANVIL, + BlockTags.FENCE_GATES, + ) + ) + + data class Checker<T>( + val direct: Set<T>, + val byTag: Set<TagKey<T>>, + ) { + fun matches(entry: Holder<T>): Boolean { + return entry.value() in direct || checkTags(entry, byTag) + } + } + + val etherwarpHallpasses = Checker( + setOf( + Blocks.CREEPER_HEAD, + Blocks.CREEPER_WALL_HEAD, + Blocks.DRAGON_HEAD, + Blocks.DRAGON_WALL_HEAD, + Blocks.SKELETON_SKULL, + Blocks.SKELETON_WALL_SKULL, + Blocks.WITHER_SKELETON_SKULL, + Blocks.WITHER_SKELETON_WALL_SKULL, + Blocks.PIGLIN_HEAD, + Blocks.PIGLIN_WALL_HEAD, + Blocks.ZOMBIE_HEAD, + Blocks.ZOMBIE_WALL_HEAD, + Blocks.PLAYER_HEAD, + Blocks.PLAYER_WALL_HEAD, + Blocks.REPEATER, + Blocks.COMPARATOR, + Blocks.BIG_DRIPLEAF_STEM, + Blocks.MOSS_CARPET, + Blocks.PALE_MOSS_CARPET, + Blocks.COCOA, + Blocks.LADDER, + Blocks.SEA_PICKLE, + ), + setOf( + BlockTags.FLOWER_POTS, + BlockTags.WOOL_CARPETS, + ), + ) + val etherwarpConsidersFat = Checker( + setOf(), setOf( + // Wall signs have a hitbox + BlockTags.ALL_SIGNS, BlockTags.ALL_HANGING_SIGNS, + BlockTags.BANNERS, + ) + ) + + + fun <T> checkTags(holder: Holder<out T>, set: Set<TagKey<out T>>) = + holder.tags() + .anyMatch(set::contains) + + + fun isEtherwarpTransparent(world: BlockGetter, blockPos: BlockPos): Boolean { + val blockState = world.getBlockState(blockPos) + val block = blockState.block + if (etherwarpConsidersFat.matches(blockState.blockHolder)) + return false + if (block.defaultBlockState().getCollisionShape(world, blockPos).isEmpty) + return true + if (etherwarpHallpasses.matches(blockState.blockHolder)) + return true + return false + } + + sealed interface EtherwarpBlockHit { + data class BlockHit(val blockPos: BlockPos, val accuratePos: Vec3?) : EtherwarpBlockHit + data object Miss : EtherwarpBlockHit + } + + fun raycastWithEtherwarpTransparency(world: BlockGetter, start: Vec3, end: Vec3): EtherwarpBlockHit { + return BlockGetter.traverseBlocks<EtherwarpBlockHit, Unit>( + start, end, Unit, + { _, blockPos -> + if (isEtherwarpTransparent(world, blockPos)) { + return@traverseBlocks null + } +// val defaultedState = world.getBlockState(blockPos).block.defaultState +// val hitShape = defaultedState.getCollisionShape( +// world, +// blockPos, +// ShapeContext.absent() +// ) +// if (world.raycastBlock(start, end, blockPos, hitShape, defaultedState) == null) { +// return@raycast null +// } + val partialResult = world.clipWithInteractionOverride(start, end, blockPos, Shapes.block(), world.getBlockState(blockPos).block.defaultBlockState()) + return@traverseBlocks EtherwarpBlockHit.BlockHit(blockPos, partialResult?.location) + }, + { EtherwarpBlockHit.Miss }) + } + + enum class EtherwarpItemKind { + MERGED, + RAW + } + + @Subscribe + fun renderEtherwarpOverlay(event: WorldRenderLastEvent) { + if (!TConfig.etherwarpOverlay) return + val player = MC.player ?: return + if (TConfig.onlyShowWhileSneaking && !player.isShiftKeyDown) return + val world = player.level + val heldItem = MC.stackInHand + val etherwarpTyp = run { + if (heldItem.extraAttributes.contains("ethermerge")) + EtherwarpItemKind.MERGED + else if (heldItem.skyBlockId == SkyBlockItems.ETHERWARP_CONDUIT) + EtherwarpItemKind.RAW + else + return + } + val playerEyeHeight = // Sneaking: 1.27 (1.21) 1.54 (1.8.9) / Upright: 1.62 (1.8.9,1.21) + if (player.isShiftKeyDown || etherwarpTyp == EtherwarpItemKind.MERGED) + (if (SBData.skyblockLocation?.isModernServer ?: false) 1.27 else 1.54) + else 1.62 + val playerEyePos = player.position.add(0.0, playerEyeHeight, 0.0) + val start = playerEyePos + val end = player.getViewVector(0F).scale(160.0).add(playerEyePos) + val hitResult = raycastWithEtherwarpTransparency( + world, + start, + end, + ) + if (hitResult !is EtherwarpBlockHit.BlockHit) return + val blockPos = hitResult.blockPos + val success = run { + if (!isEtherwarpTransparent(world, blockPos.above())) + EtherwarpResult.OCCUPIED + else if (!isEtherwarpTransparent(world, blockPos.above(2))) + EtherwarpResult.OCCUPIED + else if (playerEyePos.distanceToSqr(hitResult.accuratePos ?: blockPos.center) > 61 * 61) + EtherwarpResult.TOO_DISTANT + else if ((MC.instance.hitResult as? BlockHitResult) + ?.takeIf { it.type == HitResult.Type.BLOCK } + ?.let { interactionBlocked.matches(world.getBlockState(it.blockPos).blockHolder) } + ?: false + ) + EtherwarpResult.INTERACTION_BLOCKED + else + EtherwarpResult.SUCCESS + } + RenderInWorldContext.renderInWorld(event) { + if (TConfig.cube) + block( + blockPos, + success.color().getEffectiveColourRGB() + ) + if (TConfig.wireframe) wireframeCube(blockPos, 10f) + if (TConfig.failureText && success.label != null) { + withFacingThePlayer(blockPos.center) { + text(success.label) + } + } + } + } +} diff --git a/src/main/kotlin/features/items/recipes/ArrowWidget.kt b/src/main/kotlin/features/items/recipes/ArrowWidget.kt new file mode 100644 index 0000000..db0cf60 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ArrowWidget.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.resources.ResourceLocation + +class ArrowWidget(override var position: Point) : RecipeWidget() { + override val size: Dimension + get() = Dimension(14, 14) + + companion object { + val arrowSprite = ResourceLocation.withDefaultNamespace("container/furnace/lit_progress") + } + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + arrowSprite, + 14, + 14, + 0, + 0, + position.x, + position.y, + 14, + 14 + ) + } + +} diff --git a/src/main/kotlin/features/items/recipes/ComponentWidget.kt b/src/main/kotlin/features/items/recipes/ComponentWidget.kt new file mode 100644 index 0000000..08a2aa2 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ComponentWidget.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.recipes.RecipeLayouter +import moe.nea.firmament.util.MC + +class ComponentWidget(override var position: Point, var text: Component) : RecipeWidget(), RecipeLayouter.Updater<Component> { + override fun update(newValue: Component) { + this.text = newValue + } + + override val size: Dimension + get() = Dimension(MC.font.width(text), MC.font.lineHeight) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.drawString(MC.font, text, position.x, position.y, -1) + } +} diff --git a/src/main/kotlin/features/items/recipes/EntityWidget.kt b/src/main/kotlin/features/items/recipes/EntityWidget.kt new file mode 100644 index 0000000..4a087e5 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/EntityWidget.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.gui.entity.EntityRenderer + +class EntityWidget( + override var position: Point, + override val size: Dimension, + val entity: LivingEntity +) : RecipeWidget() { + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + EntityRenderer.renderEntity( + entity, guiGraphics, + rect.x, rect.y, + rect.width.toDouble(), rect.height.toDouble(), + mouseX.toDouble(), mouseY.toDouble() + ) + } +} diff --git a/src/main/kotlin/features/items/recipes/FireWidget.kt b/src/main/kotlin/features/items/recipes/FireWidget.kt new file mode 100644 index 0000000..565152b --- /dev/null +++ b/src/main/kotlin/features/items/recipes/FireWidget.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics + +class FireWidget(override var position: Point, val animationTicks: Int) : RecipeWidget() { + override val size: Dimension + get() = Dimension(10, 10) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/features/items/recipes/ItemList.kt b/src/main/kotlin/features/items/recipes/ItemList.kt new file mode 100644 index 0000000..340e1c3 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ItemList.kt @@ -0,0 +1,308 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.observer.Property +import java.util.Optional +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Renderable +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.navigation.ScreenAxis +import net.minecraft.client.gui.navigation.ScreenRectangle +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.input.MouseButtonInfo +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.api.v1.FirmamentAPI +import moe.nea.firmament.events.HandledScreenClickEvent +import moe.nea.firmament.events.HandledScreenForegroundEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.accessors.castAccessor +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyblockId + +object ItemList { + // TODO: add a global toggle for this and RecipeRegistry + + fun collectExclusions(screen: Screen): Set<ScreenRectangle> { + val exclusions = mutableSetOf<ScreenRectangle>() + if (screen is AbstractContainerScreen<*>) { + val screenHandler = screen.castAccessor() + exclusions.add( + ScreenRectangle( + screenHandler.x_Firmament, + screenHandler.y_Firmament, + screenHandler.backgroundWidth_Firmament, + screenHandler.backgroundHeight_Firmament + ) + ) + } + FirmamentAPI.getInstance().extensions + .forEach { extension -> + for (rectangle in extension.getExclusionZones(screen)) { + if (exclusions.any { it.encompasses(rectangle) }) + continue + exclusions.add(rectangle) + } + } + + return exclusions + } + + var reachableItems = listOf<SBItemStack>() + var pageOffset = 0 + fun recalculateVisibleItems() { + reachableItems = RepoManager.neuRepo.items + .items.values.map { SBItemStack(it.skyblockId) } + } + + @Subscribe + fun onReload(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener { recalculateVisibleItems() } + } + + fun coordinates(outer: ScreenRectangle, exclusions: Collection<ScreenRectangle>): Sequence<ScreenRectangle> { + val entryWidth = 18 + val columns = outer.width / entryWidth + val rows = outer.height / entryWidth + val lowX = outer.right() - columns * entryWidth + val lowY = outer.top() + return generateSequence(0) { it + 1 } + .map { + val xIndex = it % columns + val yIndex = it / columns + ScreenRectangle( + lowX + xIndex * entryWidth, lowY + yIndex * entryWidth, + entryWidth, entryWidth + ) + } + .take(rows * columns) + .filter { candidate -> exclusions.none { it.intersects(candidate) } } + } + + var lastRenderPositions: List<Pair<ScreenRectangle, SBItemStack>> = listOf() + var lastHoveredItemStack: Pair<ScreenRectangle, SBItemStack>? = null + + abstract class ItemListElement( + ) : Renderable, GuiEventListener { + abstract val rectangle: Rectangle + override fun setFocused(focused: Boolean) { + } + + override fun isFocused(): Boolean { + return false + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return rectangle.contains(mouseX, mouseY) + } + } + + interface HasLabel { + fun component(): Component + } + + + class PopupSettingsElement<T : HasLabel>( + x: Int, + y: Int, + width: Int, + val selected: GetSetter<T>, + val options: List<T>, + ) : ItemListElement() { + override val rectangle: Rectangle = Rectangle(x, y, width, 4 + (MC.font.lineHeight + 2) * options.size) + fun bb(i: Int) = + Rectangle( + rectangle.minX, rectangle.minY + (2 + MC.font.lineHeight) * i + 2, + rectangle.width, MC.font.lineHeight + ) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.fill(rectangle.minX, rectangle.minY, rectangle.maxX, rectangle.maxY, 0xFF000000.toInt()) + guiGraphics.submitOutline(rectangle.x, rectangle.y, rectangle.width, rectangle.height, -1) + val sel = selected.get() + for ((index, element) in options.withIndex()) { + val b = bb(index) + val tw = MC.font.width(element.component()) + guiGraphics.drawString( + MC.font, element.component(), b.centerX - tw / 2, + b.y + 1, + if (element == sel) 0xFFA0B000.toInt() else -1 + ) + if (b.contains(mouseX, mouseY)) + guiGraphics.hLine(b.centerX - tw / 2, b.centerX + tw / 2 - 1, b.maxY + 1, -1) + } + } + + override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + popupElement = null + for ((index, element) in options.withIndex()) { + val b = bb(index) + if (b.contains(event.x, event.y)) { + selected.set(element) + break + } + } + return true + } + } + + class SettingElement<T : HasLabel>( + x: Int, + y: Int, + val selected: GetSetter<T>, + val options: List<T> + ) : ItemListElement() { + val height = MC.font.lineHeight + 4 + val width = options.maxOf { MC.font.width(it.component()) } + 4 + override val rectangle: Rectangle = Rectangle(x, y, width, height) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.drawCenteredString(MC.font, selected.get().component(), rectangle.centerX, rectangle.y + 2, -1) + if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) { + guiGraphics.hLine(rectangle.minX, rectangle.maxX - 1, rectangle.maxY - 2, -1) + } + } + + override fun mouseClicked( + event: MouseButtonEvent, + isDoubleClick: Boolean + ): Boolean { + popupElement = PopupSettingsElement( + rectangle.x, + rectangle.y - options.size * (MC.font.lineHeight + 2) - 2, + width, + selected, + options + ) + return true + } + } + + var popupElement: ItemListElement? = null + + + fun findStackUnder(mouseX: Int, mouseY: Int): Pair<ScreenRectangle, SBItemStack>? { + val lhis = lastHoveredItemStack + if (lhis != null && lhis.first.containsPoint(mouseX, mouseY)) + return lhis + return lastRenderPositions.firstOrNull { it.first.containsPoint(mouseX, mouseY) } + } + + val isItemListEnabled get() = false + + @Subscribe + fun onClick(event: HandledScreenClickEvent) { + if(!isItemListEnabled)return + val pe = popupElement + val me = MouseButtonEvent( + event.mouseX, event.mouseY, + MouseButtonInfo(event.button, 0) // TODO: missing modifiers + ) + if (pe != null) { + event.cancel() + if (!pe.isMouseOver(event.mouseX, event.mouseY)) { + popupElement = null + return + } + pe.mouseClicked( + me, + false + ) + return + } + listElements.forEach { + if (it.isMouseOver(event.mouseX, event.mouseY)) + it.mouseClicked(me, false) + } + } + + var listElements = listOf<ItemListElement>() + + @Subscribe + fun onRender(event: HandledScreenForegroundEvent) { + if(!isItemListEnabled) return + lastHoveredItemStack = null + lastRenderPositions = listOf() + val exclusions = collectExclusions(event.screen) + val potentiallyVisible = reachableItems.subList(pageOffset, reachableItems.size) + val screenWidth = event.screen.width + val rightThird = ScreenRectangle( + screenWidth - screenWidth / 3, 0, + screenWidth / 3, event.screen.height - MC.font.lineHeight - 4 + ) + val coords = coordinates(rightThird, exclusions) + + lastRenderPositions = coords.zip(potentiallyVisible.asSequence()).toList() + val isPopupHovered = popupElement?.isMouseOver(event.mouseX.toDouble(),event.mouseY.toDouble()) + ?: false + lastRenderPositions.forEach { (pos, stack) -> + val realStack = stack.asLazyImmutableItemStack() + val toRender = realStack ?: ItemStack(Items.PAINTING) + event.context.renderItem(toRender, pos.left() + 1, pos.top() + 1) + if (!isPopupHovered && pos.containsPoint(event.mouseX, event.mouseY)) { + lastHoveredItemStack = pos to stack + event.context.setTooltipForNextFrame( + MC.font, + if (realStack != null) + ItemSlotWidget.getTooltip(realStack) + else + stack.estimateLore(), + Optional.empty(), + event.mouseX, event.mouseY + ) + } + } + event.context.fill( + rightThird.left(), + rightThird.bottom(), + rightThird.right(), + event.screen.height, + 0xFF000000.toInt() + ) + val le = mutableListOf<ItemListElement>() + le.add( + SettingElement( + 0, + rightThird.bottom(), + sortOrder, + SortOrder.entries + ) + ) + val bottomWidth = le.sumOf { it.rectangle.width + 2 } - 2 + var startX = rightThird.getCenterInAxis(ScreenAxis.HORIZONTAL) - bottomWidth / 2 + le.forEach { + it.rectangle.translate(startX, 0) + startX += it.rectangle.width + 2 + } + le.forEach { it.render(event.context, event.mouseX, event.mouseY, event.delta) } + listElements = le + popupElement?.render(event.context, event.mouseX, event.mouseY, event.delta) + } + + enum class SortOrder(val component: Component) : HasLabel { + NAME(Component.literal("Name")), + RARITY(Component.literal("Rarity")); + + override fun component(): Component = component + } + + val sortOrder = Property.of(SortOrder.NAME) +} diff --git a/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt new file mode 100644 index 0000000..b659643 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt @@ -0,0 +1,141 @@ +package moe.nea.firmament.features.items.recipes + +import java.util.Optional +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.TooltipFlag +import moe.nea.firmament.api.v1.FirmamentItemWidget +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.FirmFormatters.shortFormat +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt + +class ItemSlotWidget( + point: Point, + var content: List<SBItemStack>, + val slotKind: RecipeLayouter.SlotKind +) : RecipeWidget(), + RecipeLayouter.CyclingItemSlot, + FirmamentItemWidget { + override var position = point + override val size get() = Dimension(16, 16) + val itemRect get() = Rectangle(position, Dimension(16, 16)) + + val backgroundTopLeft + get() = + if (slotKind.isBig) Point(position.x - 4, position.y - 4) + else Point(position.x - 1, position.y - 1) + val backgroundSize = + if (slotKind.isBig) Dimension(16 + 8, 16 + 8) + else Dimension(18, 18) + override val rect: Rectangle + get() = Rectangle(backgroundTopLeft, backgroundSize) + + @OptIn(ExpensiveItemCacheApi::class) + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + val stack = current().asImmutableItemStack() + // TODO: draw slot background + if (stack.isEmpty) return + guiGraphics.renderItem(stack, position.x, position.y) + guiGraphics.renderItemDecorations( + MC.font, stack, position.x, position.y, + if (stack.count >= SHORT_NUM_CUTOFF) shortFormat(stack.count.toDouble()) + else null + ) + if (itemRect.contains(mouseX, mouseY) + && guiGraphics.containsPointInScissor(mouseX, mouseY) + ) guiGraphics.setTooltipForNextFrame( + MC.font, getTooltip(stack), Optional.empty(), + mouseX, mouseY + ) + } + + companion object { + val SHORT_NUM_CUTOFF = 1000 + var canUseTooltipEvent = true + + fun getTooltip(itemStack: ItemStack): List<Component> { + val lore = mutableListOf(itemStack.displayNameAccordingToNbt) + lore.addAll(itemStack.loreAccordingToNbt) + if (canUseTooltipEvent) { + try { + ItemTooltipCallback.EVENT.invoker().getTooltip( + itemStack, Item.TooltipContext.EMPTY, + TooltipFlag.NORMAL, lore + ) + } catch (ex: Exception) { + canUseTooltipEvent = false + ErrorUtil.softError("Failed to use vanilla tooltips", ex) + } + } else { + ItemTooltipEvent.publish( + ItemTooltipEvent( + itemStack, + Item.TooltipContext.EMPTY, + TooltipFlag.NORMAL, + lore + ) + ) + } + if (itemStack.count >= SHORT_NUM_CUTOFF && lore.isNotEmpty()) + lore.add(1, Component.literal("${itemStack.count}x").darkGrey()) + return lore + } + } + + + override fun tick() { + if (SavedKeyBinding.isShiftDown()) return + if (content.size <= 1) return + if (MC.currentTick % 5 != 0) return + index = (index + 1) % content.size + } + + var index = 0 + var onUpdate: () -> Unit = {} + + override fun onUpdate(action: () -> Unit) { + this.onUpdate = action + } + + override fun current(): SBItemStack { + return content.getOrElse(index) { SBItemStack.EMPTY } + } + + override fun update(newValue: SBItemStack) { + content = listOf(newValue) + // SAFE: content was just assigned to a non-empty list + index = index.coerceIn(content.indices) + } + + override fun getPlacement(): FirmamentItemWidget.Placement { + return FirmamentItemWidget.Placement.RECIPE_SCREEN + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun getItemStack(): ItemStack { + return current().asImmutableItemStack() + } + + override fun getSkyBlockId(): String { + return current().skyblockId.neuItem + } +} diff --git a/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt new file mode 100644 index 0000000..aad3bda --- /dev/null +++ b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt @@ -0,0 +1,46 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.input.MouseButtonEvent +import moe.nea.firmament.util.MoulConfigUtils.createAndTranslateFullContext + +class MoulConfigWidget( + val component: GuiComponent, + override var position: Point, + override val size: Dimension, +) : RecipeWidget() { + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + createAndTranslateFullContext( + guiGraphics, mouseX, mouseY, rect, + component::render + ) + } + + override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + return createAndTranslateFullContext(null, event.x.toInt(), event.y.toInt(), rect) { + component.mouseEvent(MouseEvent.Click(event.button(), true), it) + } + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + createAndTranslateFullContext(null, mouseX, mouseY, rect) { + component.mouseEvent(MouseEvent.Move(0F, 0F), it) + } + } + + override fun mouseReleased(event: MouseButtonEvent): Boolean { + return createAndTranslateFullContext(null, event.x, event.y, rect) { + component.mouseEvent(MouseEvent.Click(event.button(), false), it) + } + } + +} diff --git a/src/main/kotlin/features/items/recipes/RecipeRegistry.kt b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt new file mode 100644 index 0000000..c2df46f --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt @@ -0,0 +1,116 @@ +package moe.nea.firmament.features.items.recipes + +import com.mojang.blaze3d.platform.InputConstants +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer +import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer +import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer +import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer +import moe.nea.firmament.repo.recipes.SBReforgeRecipeRenderer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.focusedItemStack + +object RecipeRegistry { + val recipeTypes: List<GenericRecipeRenderer<*>> = listOf( + SBCraftingRecipeRenderer, + SBForgeRecipeRenderer, + SBReforgeRecipeRenderer, + SBEssenceUpgradeRecipeRenderer, + ) + + + @Subscribe + fun showUsages(event: HandledScreenKeyPressedEvent) { + val provider = + if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_R))) { + ::getRecipesFor + } else if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_U))) { + ::getUsagesFor + } else { + return + } + val stack = event.screen.focusedItemStack ?: return + val recipes = provider(SBItemStack(stack)) + if (recipes.isEmpty()) return + MC.screen = RecipeScreen(recipes.toList()) + } + + + object RecipeIndexes : IReloadable { + + private fun <T : Any> createIndexFor( + neuRepository: NEURepository, + recipeRenderer: GenericRecipeRenderer<T>, + outputs: Boolean, + ): List<Pair<SkyblockId, RenderableRecipe<T>>> { + val indexer: (T) -> Collection<SBItemStack> = + if (outputs) recipeRenderer::getOutputs + else recipeRenderer::getInputs + return recipeRenderer.findAllRecipes(neuRepository) + .flatMap { + val wrappedRecipe = RenderableRecipe(it, recipeRenderer, null) + indexer(it).map { it.skyblockId to wrappedRecipe } + } + } + + fun createIndex(outputs: Boolean): MutableMap<SkyblockId, List<RenderableRecipe<*>>> { + val m: MutableMap<SkyblockId, List<RenderableRecipe<*>>> = mutableMapOf() + recipeTypes.forEach { renderer -> + createIndexFor(RepoManager.neuRepo, renderer, outputs) + .forEach { (stack, recipe) -> + m.merge(stack, listOf(recipe)) { a, b -> a + b } + } + } + return m + } + + lateinit var recipesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>> + lateinit var usagesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>> + override fun reload(recipe: NEURepository) { + recipesForIndex = createIndex(true) + usagesForIndex = createIndex(false) + } + } + + @Subscribe + fun onRepoBuild(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener(RecipeIndexes) + } + + + fun getRecipesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> { + val recipes = LinkedHashSet<RenderableRecipe<*>>() + recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, true) } + recipes.addAll(RecipeIndexes.recipesForIndex[itemStack.skyblockId] ?: emptyList()) + return recipes + } + + fun getUsagesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> { + val recipes = LinkedHashSet<RenderableRecipe<*>>() + recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, false) } + recipes.addAll(RecipeIndexes.usagesForIndex[itemStack.skyblockId] ?: emptyList()) + return recipes + } + + private fun <T : Any> injectRecipesFor( + recipeRenderer: GenericRecipeRenderer<T>, + collector: MutableCollection<RenderableRecipe<*>>, + relevantItem: SBItemStack, + mustBeInOutputs: Boolean + ) { + collector.addAll( + recipeRenderer.discoverExtraRecipes(RepoManager.neuRepo, relevantItem, mustBeInOutputs) + .map { RenderableRecipe(it, recipeRenderer, relevantItem) } + ) + } + + +} diff --git a/src/main/kotlin/features/items/recipes/RecipeScreen.kt b/src/main/kotlin/features/items/recipes/RecipeScreen.kt new file mode 100644 index 0000000..9a746f3 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeScreen.kt @@ -0,0 +1,129 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.renderer.RenderPipelines +import moe.nea.firmament.util.mc.CommonTextures +import moe.nea.firmament.util.render.enableScissorWithTranslation +import moe.nea.firmament.util.tr + +class RecipeScreen( + val recipes: List<RenderableRecipe<*>>, +) : Screen(tr("firmament.recipe.screen", "SkyBlock Recipe")) { + + data class PlacedRecipe( + val bounds: Rectangle, + val layoutedRecipe: StandaloneRecipeRenderer, + ) { + fun moveTo(position: Point) { + val Δx = position.x - bounds.x + val Δy = position.y - bounds.y + bounds.translate(Δx, Δy) + layoutedRecipe.widgets.forEach { widget -> + widget.position = widget.position.clone().also { + it.translate(Δx, Δy) + } + } + } + } + + lateinit var placedRecipes: List<PlacedRecipe> + var scrollViewport: Int = 0 + var scrollOffset: Int = 0 + var scrollPortWidth: Int = 0 + var heightEstimate: Int = 0 + val gutter = 10 + override fun init() { + super.init() + scrollViewport = minOf(height - 20, 250) + scrollPortWidth = 0 + heightEstimate = 0 + var offset = height / 2 - scrollViewport / 2 + placedRecipes = recipes.map { + val effectiveWidth = minOf(it.renderer.displayWidth, width - 20) + val bounds = Rectangle( + width / 2 - effectiveWidth / 2, + offset, + effectiveWidth, + it.renderer.displayHeight + ) + if (heightEstimate > 0) + heightEstimate += gutter + heightEstimate += bounds.height + scrollPortWidth = maxOf(effectiveWidth, scrollPortWidth) + offset += bounds.height + gutter + val layoutedRecipe = it.render(bounds) + layoutedRecipe.widgets.forEach(this::addRenderableWidget) + PlacedRecipe(bounds, layoutedRecipe) + } + } + + fun scrollRect() = + Rectangle( + width / 2 - scrollPortWidth / 2, height / 2 - scrollViewport / 2, + scrollPortWidth, scrollViewport + ) + + fun scissorScrollPort(guiGraphics: GuiGraphics) { + guiGraphics.enableScissorWithTranslation(scrollRect()) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean { + if (!scrollRect().contains(mouseX, mouseY)) + return false + scrollOffset = (scrollOffset + scrollY * -4) + .coerceAtMost(heightEstimate - scrollViewport.toDouble()) + .coerceAtLeast(.0) + .toInt() + var offset = height / 2 - scrollViewport / 2 - scrollOffset + placedRecipes.forEach { + it.moveTo(Point(it.bounds.x, offset)) + offset += it.bounds.height + gutter + } + return true + } + + override fun renderBackground( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + super.renderBackground(guiGraphics, mouseX, mouseY, partialTick) + + val srect = scrollRect() + srect.grow(8, 8) + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + CommonTextures.genericWidget(), + srect.x, srect.y, + srect.width, srect.height + ) + + scissorScrollPort(guiGraphics) + placedRecipes.forEach { + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + CommonTextures.genericWidget(), + it.bounds.x, it.bounds.y, + it.bounds.width, it.bounds.height + ) + } + guiGraphics.disableScissor() + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + scissorScrollPort(guiGraphics) + super.render(guiGraphics, mouseX, mouseY, partialTick) + guiGraphics.disableScissor() + } + + override fun tick() { + super.tick() + placedRecipes.forEach { + it.layoutedRecipe.tick() + } + } +} diff --git a/src/main/kotlin/features/items/recipes/RecipeWidget.kt b/src/main/kotlin/features/items/recipes/RecipeWidget.kt new file mode 100644 index 0000000..f13707c --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeWidget.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.components.Renderable +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.narration.NarratableEntry +import net.minecraft.client.gui.narration.NarrationElementOutput +import net.minecraft.client.gui.navigation.ScreenRectangle +import moe.nea.firmament.util.mc.asScreenRectangle + +abstract class RecipeWidget : GuiEventListener, Renderable, NarratableEntry { + override fun narrationPriority(): NarratableEntry.NarrationPriority? { + return NarratableEntry.NarrationPriority.NONE// I am so sorry + } + + override fun updateNarration(narrationElementOutput: NarrationElementOutput) { + } + + open fun tick() {} + private var _focused = false + abstract var position: Point + abstract val size: Dimension + open val rect: Rectangle get() = Rectangle(position, size) + override fun setFocused(focused: Boolean) { + this._focused = focused + } + + override fun isFocused(): Boolean { + return this._focused + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return rect.contains(mouseX, mouseY) + } +} diff --git a/src/main/kotlin/features/items/recipes/RenderableRecipe.kt b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt new file mode 100644 index 0000000..20ca17e --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import java.util.Objects +import me.shedaniel.math.Rectangle +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer + +class RenderableRecipe<T : Any>( + val recipe: T, + val renderer: GenericRecipeRenderer<T>, + val mainItemStack: SBItemStack?, +) { + fun render(bounds: Rectangle): StandaloneRecipeRenderer { + val layouter = StandaloneRecipeRenderer(bounds) + renderer.render(recipe, bounds, layouter, mainItemStack) + return layouter + } + +// override fun equals(other: Any?): Boolean { +// if (other !is RenderableRecipe<*>) return false +// return renderer == other.renderer && recipe == other.recipe +// } +// +// override fun hashCode(): Int { +// return Objects.hash(recipe, renderer) +// } +} diff --git a/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt new file mode 100644 index 0000000..5a834eb --- /dev/null +++ b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt @@ -0,0 +1,77 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.components.events.AbstractContainerEventHandler +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class StandaloneRecipeRenderer(val bounds: Rectangle) : AbstractContainerEventHandler(), RecipeLayouter { + + fun tick() { + widgets.forEach { it.tick() } + } + + fun <T : RecipeWidget> addWidget(widget: T): T { + this.widgets.add(widget) + return widget + } + + override fun createCyclingItemSlot( + x: Int, + y: Int, + content: List<SBItemStack>, + slotKind: RecipeLayouter.SlotKind + ): RecipeLayouter.CyclingItemSlot { + return addWidget(ItemSlotWidget(Point(x, y), content, slotKind)) + } + + val Rectangle.topLeft get() = Point(x, y) + + override fun createTooltip( + rectangle: Rectangle, + label: List<Component> + ) { + addWidget(TooltipWidget(rectangle.topLeft, rectangle.size, label)) + } + + override fun createLabel( + x: Int, + y: Int, + text: Component + ): RecipeLayouter.Updater<Component> { + return addWidget(ComponentWidget(Point(x, y), text)) + } + + override fun createArrow(x: Int, y: Int): Rectangle { + return addWidget(ArrowWidget(Point(x, y))).rect + } + + override fun createMoulConfig( + x: Int, + y: Int, + w: Int, + h: Int, + component: GuiComponent + ) { + addWidget(MoulConfigWidget(component, Point(x, y), Dimension(w, h))) + } + + override fun createFire(point: Point, animationTicks: Int) { + addWidget(FireWidget(point, animationTicks)) + } + + override fun createEntity(rectangle: Rectangle, entity: LivingEntity) { + addWidget(EntityWidget(rectangle.topLeft, rectangle.size, entity)) + } + + val widgets: MutableList<RecipeWidget> = mutableListOf() + override fun children(): List<GuiEventListener> { + return widgets + } +} diff --git a/src/main/kotlin/features/items/recipes/TooltipWidget.kt b/src/main/kotlin/features/items/recipes/TooltipWidget.kt new file mode 100644 index 0000000..87feb61 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/TooltipWidget.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class TooltipWidget( + override var position: Point, + override val size: Dimension, + label: List<Component> +) : RecipeWidget(), RecipeLayouter.Updater<List<Component>> { + override fun update(newValue: List<Component>) { + this.formattedComponent = newValue.map { it.visualOrderText } + } + + var formattedComponent = label.map { it.visualOrderText } + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + if (rect.contains(mouseX, mouseY)) + guiGraphics.setTooltipForNextFrame(formattedComponent, mouseX, mouseY) + } + +} diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt new file mode 100644 index 0000000..9dadb80 --- /dev/null +++ b/src/main/kotlin/features/macros/ComboProcessor.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.features.macros + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.network.chat.Component +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>() + + 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.pose().pushMatrix() + val width = 120 + event.context.pose().translate( + (MC.window.guiScaledWidth - width) / 2F, + (MC.window.guiScaledHeight) / 2F + 8 + ) + val breadCrumbText = breadCrumbs.joinToString(" > ") + event.context.drawString( + MC.font, + tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText), + 0, + 0, + -1, + true + ) + event.context.pose().translate(0F, MC.font.lineHeight + 2F) + for ((key, value) in activeTrie.nodes) { + event.context.drawString( + MC.font, + Component.literal("$breadCrumbText > $key: ").append(value.label), + 0, + 0, + -1, + true + ) + event.context.pose().translate(0F, MC.font.lineHeight + 1F) + } + event.context.pose().popMatrix() + } + + @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..18c95bc --- /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.network.chat.Component +import moe.nea.firmament.util.MC + +@Serializable +sealed interface HotkeyAction { + // TODO: execute + val label: Component + fun execute() +} + +@Serializable +@SerialName("command") +data class CommandAction(val command: String) : HotkeyAction { + override val label: Component + get() = Component.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..2701ac1 --- /dev/null +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -0,0 +1,73 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import net.minecraft.network.chat.Component +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil + +sealed interface KeyComboTrie { + val label: Component + + companion object { + fun fromComboList( + combos: List<ComboKeyAction>, + ): Branch { + val root = Branch(mutableMapOf()) + for (combo in combos) { + var p = root + if (combo.keySequence.isEmpty()) { + ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty") + continue + } + for ((index, key) in combo.keySequence.withIndex()) { + val m = (p.nodes as MutableMap) + if (index == combo.keySequence.lastIndex) { + if (key in m) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence.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.keySequence} (final node exists at index $index) through another action already") + break + } else { + p = c + } + } + } + } + return root + } + } +} + +@Serializable +data class MacroWheel( + val keyBinding: SavedKeyBinding = SavedKeyBinding.unbound(), + val options: List<HotkeyAction> +) + +@Serializable +data class ComboKeyAction( + val action: HotkeyAction, + val keySequence: List<SavedKeyBinding> = listOf(), +) + +data class Leaf(val action: HotkeyAction) : KeyComboTrie { + override val label: Component + get() = action.label + + fun execute() { + action.execute() + } +} + +data class Branch( + val nodes: Map<SavedKeyBinding, KeyComboTrie> +) : KeyComboTrie { + override val label: Component + get() = Component.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..af1b0e8 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.DataHolder + +@Serializable +data class MacroData( + var comboActions: List<ComboKeyAction> = listOf(), + var wheels: List<MacroWheel> = listOf(), +) { + @Config + object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) { + override fun onLoad() { + ComboProcessor.setActions(data.comboActions) + RadialMacros.setWheels(data.wheels) + } + } +} diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt new file mode 100644 index 0000000..e73f076 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -0,0 +1,293 @@ +package moe.nea.firmament.features.macros + +import io.github.notenoughupdates.moulconfig.common.text.StructuredText +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +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() + + @field:Bind("wheels") + val wheels = Wheels() + var dontSave = false + + @Bind + fun beforeClose(): CloseEventListener.CloseAction { + if (!dontSave) + save() + return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE + } + + fun save() { + MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() } + MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() } + MacroData.DConfig.markDirty() + RadialMacros.setWheels(MacroData.DConfig.data.wheels) + ComboProcessor.setActions(MacroData.DConfig.data.comboActions) + } + + fun discard() { + dontSave = true + MC.screen?.onClose() + } + + class Command( + @field:Bind("text") + var text: String, + val parent: Wheel, + ) { + @Bind + fun textR() = StructuredText.of(text) + + @Bind + fun delete() { + parent.editableCommands.removeIf { it === this } + parent.editableCommands.update() + parent.commands.update() + } + + fun asCommandAction() = CommandAction(text) + } + + inner class Wheel( + val parent: Wheels, + var binding: SavedKeyBinding, + commands: List<CommandAction>, + ) { + + fun asSaveable(): MacroWheel { + return MacroWheel(binding, commands.map { it.asCommandAction() }) + } + + @Bind("keyCombo") + fun text() = MoulConfigPlatform.wrap(binding.format()) + + @field:Bind("commands") + val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) } + + @field:Bind("editableCommands") + val editableCommands = this.commands.toObservableList() + + @Bind + fun addOption() { + editableCommands.add(Command("", this)) + } + + @Bind + fun back() { + MC.screen?.onClose() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen) + } + + @Bind + fun delete() { + parent.wheels.removeIf { it === this } + parent.wheels.update() + } + + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + + fun requestFocus() { + button.requestFocus() + } + } + + inner class Wheels { + @field:Bind("wheels") + val wheels: ObservableList<Wheel> = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) { + Wheel(this, it.keyBinding, it.options.map { CommandAction((it as CommandAction).command) }) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + + @Bind + fun addWheel() { + wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf())) + } + } + + fun saveAndClose() { + save() + MC.screen?.onClose() + } + + inner class Combos { + @field:Bind("actions") + val actions: ObservableList<ActionEditor> = ObservableList( + MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { + ActionEditor(it, this) + } + ) + + @Bind + fun addCommand() { + actions.add( + ActionEditor( + ComboKeyAction( + CommandAction("ac Hello from a Firmament Hotkey"), + listOf() + ), + this + ) + ) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + } + + 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() + } + + + fun requestFocus() { + button.requestFocus() + } + + @Bind + fun delete() { + parent.combo.removeIf { it === this } + parent.combo.update() + } + } + + class ActionEditor(val action: ComboKeyAction, val parent: Combos) { + fun asSaveable(): ComboKeyAction { + return ComboKeyAction( + CommandAction(command), + combo.map { it.binding } + ) + } + + @field:Bind("command") + var command: String = (action.action as CommandAction).command + + @Bind + fun commandR() = StructuredText.of(command) + + @field:Bind("combo") + val combo = action.keySequence.map { KeyBindingEditor(it, this) }.toObservableList() + + @Bind + fun formattedCombo() = + StructuredText.of(combo.joinToString(" > ") { it.binding.toString() }) // TODO: this can be joined without .toString() + + @Bind + fun addStep() { + combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this)) + } + + @Bind + fun back() { + MC.screen?.onClose() + } + + @Bind + fun delete() { + parent.actions.removeIf { it === this } + parent.actions.update() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", 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/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt new file mode 100644 index 0000000..2519123 --- /dev/null +++ b/src/main/kotlin/features/macros/RadialMenu.kt @@ -0,0 +1,153 @@ +package moe.nea.firmament.features.macros + +import me.shedaniel.math.Color +import org.joml.Vector2f +import util.render.CustomRenderLayers +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt +import net.minecraft.client.gui.GuiGraphics +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.events.WorldMouseMoveEvent +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderCircleProgress +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.render.lerpAngle +import moe.nea.firmament.util.render.wrapAngle +import moe.nea.firmament.util.render.τ + +object RadialMenuViewer { + interface RadialMenu { + val key: SavedKeyBinding + val options: List<RadialMenuOption> + } + + interface RadialMenuOption { + val isEnabled: Boolean + fun resolve() + fun renderSlice(drawContext: GuiGraphics) + } + + var activeMenu: RadialMenu? = null + set(value) { + if (value?.options.isNullOrEmpty()) { + field = null + } else { + field = value + } + delta = Vector2f(0F, 0F) + } + var delta = Vector2f(0F, 0F) + val maxSelectionSize = 100F + + @Subscribe + fun onMouseMotion(event: WorldMouseMoveEvent) { + val menu = activeMenu ?: return + event.cancel() + delta.add(event.deltaX.toFloat(), event.deltaY.toFloat()) + val m = delta.lengthSquared() + if (m > maxSelectionSize * maxSelectionSize) { + delta.mul(maxSelectionSize / sqrt(m)) + } + } + + val INNER_CIRCLE_RADIUS = 16 + + @Subscribe + fun onRender(event: HudRenderEvent) { + val menu = activeMenu ?: return + val mat = event.context.pose() + mat.pushMatrix() + mat.translate( + (MC.window.guiScaledWidth) / 2F, + (MC.window.guiScaledHeight) / 2F, + ) + val sliceWidth = (τ / menu.options.size).toFloat() + var selectedAngle = wrapAngle(atan2(delta.y, delta.x)) + if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS) + selectedAngle = Float.NaN + for ((idx, option) in menu.options.withIndex()) { + val range = (sliceWidth * idx)..(sliceWidth * (idx + 1)) + mat.pushMatrix() + mat.scale(64F, 64F) + val cutout = INNER_CIRCLE_RADIUS / 64F / 2 + RenderCircleProgress.renderCircularSlice( + event.context, + CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI, + 0F, 1F, 0F, 1F, + range, + color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF, + innerCutoutRadius = cutout + ) + mat.popMatrix() + mat.pushMatrix() + val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F) + val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F) + mat.translate(vec.x, vec.y) + option.renderSlice(event.context) + mat.popMatrix() + } + event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), Color.ofOpaque(0x00FF00)) + mat.popMatrix() + } + + @Subscribe + fun onTick(event: TickEvent) { + val menu = activeMenu ?: return + if (!menu.key.isPressed(true)) { + val angle = atan2(delta.y, delta.x) + + val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt() + val choice = menu.options[choiceIndex] + val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS + activeMenu = null + if (selectedAny) + choice.resolve() + } + } + +} + +object RadialMacros { + lateinit var wheels: List<MacroWheel> + private set + + fun setWheels(wheels: List<MacroWheel>) { + this.wheels = wheels + RadialMenuViewer.activeMenu = null + } + + @Subscribe + fun onOpen(event: WorldKeyboardEvent) { + if (RadialMenuViewer.activeMenu != null) return + wheels.forEach { wheel -> + if (event.matches(wheel.keyBinding, atLeast = true)) { + class R(val action: HotkeyAction) : RadialMenuOption { + override val isEnabled: Boolean + get() = true + + override fun resolve() { + action.execute() + } + + override fun renderSlice(drawContext: GuiGraphics) { + drawContext.drawCenteredString(MC.font, action.label, 0, 0, -1) + } + } + RadialMenuViewer.activeMenu = object : RadialMenu { + override val key: SavedKeyBinding + get() = wheel.keyBinding + override val options: List<RadialMenuOption> = + wheel.options.map { R(it) } + } + } + } + } +} diff --git a/src/main/kotlin/features/mining/CommissionFeatures.kt b/src/main/kotlin/features/mining/CommissionFeatures.kt index faba253..bfc635a 100644 --- a/src/main/kotlin/features/mining/CommissionFeatures.kt +++ b/src/main/kotlin/features/mining/CommissionFeatures.kt @@ -3,22 +3,24 @@ package moe.nea.firmament.features.mining import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.unformattedString object CommissionFeatures { - object Config : ManagedConfig("commissions", Category.MINING) { + @Config + object TConfig : ManagedConfig("commissions", Category.MINING) { val highlightCompletedCommissions by toggle("highlight-completed") { true } } @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { - if (!Config.highlightCompletedCommissions) return + if (!TConfig.highlightCompletedCommissions) return if (MC.screenName != "Commissions") return - val stack = event.slot.stack + val stack = event.slot.item if (stack.loreAccordingToNbt.any { it.unformattedString == "COMPLETED" }) { event.highlight(Firmament.identifier("completed_commission_background")) } diff --git a/src/main/kotlin/features/mining/Histogram.kt b/src/main/kotlin/features/mining/Histogram.kt index ed48437..08ee893 100644 --- a/src/main/kotlin/features/mining/Histogram.kt +++ b/src/main/kotlin/features/mining/Histogram.kt @@ -1,7 +1,8 @@ package moe.nea.firmament.features.mining -import java.util.* +import java.util.NavigableMap +import java.util.TreeMap import kotlin.time.Duration import moe.nea.firmament.util.TimeMark diff --git a/src/main/kotlin/features/mining/HotmPresets.kt b/src/main/kotlin/features/mining/HotmPresets.kt index 2241fee..4930f90 100644 --- a/src/main/kotlin/features/mining/HotmPresets.kt +++ b/src/main/kotlin/features/mining/HotmPresets.kt @@ -3,14 +3,15 @@ package moe.nea.firmament.features.mining import me.shedaniel.math.Rectangle import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.seconds -import net.minecraft.block.Blocks -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.ingame.HandledScreen -import net.minecraft.entity.player.PlayerInventory -import net.minecraft.item.Items -import net.minecraft.screen.GenericContainerScreenHandler -import net.minecraft.screen.slot.Slot -import net.minecraft.text.Text +import net.minecraft.world.level.block.Blocks +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.item.Items +import net.minecraft.world.inventory.ChestMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.commands.thenExecute @@ -18,12 +19,12 @@ import moe.nea.firmament.events.ChestInventoryUpdateEvent import moe.nea.firmament.events.CommandEvent import moe.nea.firmament.events.ScreenChangeEvent import moe.nea.firmament.events.SlotRenderEvents -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.TemplateUtil import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.accessors.castAccessor import moe.nea.firmament.util.customgui.CustomGui import moe.nea.firmament.util.customgui.customGui import moe.nea.firmament.util.mc.CommonTextures @@ -51,8 +52,8 @@ object HotmPresets { fun onScreenOpen(event: ScreenChangeEvent) { val title = event.new?.title?.unformattedString if (title != hotmInventoryName) return - val screen = event.new as? HandledScreen<*> ?: return - val oldHandler = (event.old as? HandledScreen<*>)?.customGui + val screen = event.new as? AbstractContainerScreen<*> ?: return + val oldHandler = (event.old as? AbstractContainerScreen<*>)?.customGui if (oldHandler is HotmScrollPrompt) { event.new.customGui = oldHandler oldHandler.setNewScreen(screen) @@ -63,32 +64,32 @@ object HotmPresets { screen.customGui = HotmScrollPrompt(screen) } - class HotmScrollPrompt(var screen: HandledScreen<*>) : CustomGui() { + class HotmScrollPrompt(var screen: AbstractContainerScreen<*>) : CustomGui() { var bounds = Rectangle( 0, 0, 0, 0 ) - fun setNewScreen(screen: HandledScreen<*>) { + fun setNewScreen(screen: AbstractContainerScreen<*>) { this.screen = screen onInit() hasScrolled = false } - override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + override fun render(drawContext: GuiGraphics, delta: Float, mouseX: Int, mouseY: Int) { drawContext.drawGuiTexture( CommonTextures.genericWidget(), bounds.x, bounds.y, bounds.width, bounds.height, ) - drawContext.drawCenteredTextWithShadow( + drawContext.drawCenteredString( MC.font, if (hasAll) { - Text.translatable("firmament.hotmpreset.copied") + Component.translatable("firmament.hotmpreset.copied") } else if (!hasScrolled) { - Text.translatable("firmament.hotmpreset.scrollprompt") + Component.translatable("firmament.hotmpreset.scrollprompt") } else { - Text.translatable("firmament.hotmpreset.scrolled") + Component.translatable("firmament.hotmpreset.scrolled") }, bounds.centerX, bounds.centerY - 5, @@ -100,14 +101,14 @@ object HotmPresets { var hasScrolled = false var hasAll = false - override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + override fun mouseClick(click: MouseButtonEvent, doubled: Boolean): Boolean { if (!hasScrolled) { - val slot = screen.screenHandler.getSlot(8) - println("Clicking ${slot.stack}") - slot.clickRightMouseButton(screen.screenHandler) + val slot = screen.menu.getSlot(8) + println("Clicking ${slot.item}") + slot.clickRightMouseButton(screen.menu) } hasScrolled = true - return super.mouseClick(mouseX, mouseY, button) + return super.mouseClick(click, doubled) } override fun shouldDrawForeground(): Boolean { @@ -124,7 +125,7 @@ object HotmPresets { screen.height / 2 - 100, 300, 200 ) - val screen = screen as AccessorHandledScreen + val screen = screen.castAccessor() screen.x_Firmament = bounds.x screen.y_Firmament = bounds.y screen.backgroundWidth_Firmament = bounds.width @@ -140,10 +141,10 @@ object HotmPresets { val allRows = (1..10).toSet() fun onNewItems(event: ChestInventoryUpdateEvent) { - val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return + val handler = screen.menu as? ChestMenu ?: return for (it in handler.slots) { - if (it.inventory is PlayerInventory) continue - val stack = it.stack + if (it.container is Inventory) continue + val stack = it.item val name = stack.displayNameAccordingToNbt.unformattedString tierRegex.useMatch(name) { coveredRows.add(group("tier").toInt()) @@ -156,7 +157,9 @@ object HotmPresets { } } if (allRows == coveredRows) { - ClipboardUtils.setTextContent(TemplateUtil.encodeTemplate(SHARE_PREFIX, HotmPreset( + ClipboardUtils.setTextContent( + TemplateUtil.encodeTemplate( + SHARE_PREFIX, HotmPreset( unlockedPerks.map { PerkPreset(it) } ))) hasAll = true @@ -169,7 +172,7 @@ object HotmPresets { @Subscribe fun onSlotUpdates(event: ChestInventoryUpdateEvent) { - val customGui = (event.inventory as? HandledScreen<*>)?.customGui + val customGui = (event.inventory as? AbstractContainerScreen<*>)?.customGui if (customGui is HotmScrollPrompt) { customGui.onNewItems(event) } @@ -185,7 +188,7 @@ object HotmPresets { @Subscribe fun onSlotRender(event: SlotRenderEvents.Before) { if (hotmInventoryName == MC.screenName - && event.slot.stack.displayNameAccordingToNbt.unformattedString in highlightedPerks + && event.slot.item.displayNameAccordingToNbt.unformattedString in highlightedPerks ) { event.highlight((Firmament.identifier("hotm_perk_preset"))) } @@ -197,7 +200,7 @@ object HotmPresets { thenExecute { hotmCommandSent = TimeMark.now() MC.sendCommand("hotm") - source.sendFeedback(Text.translatable("firmament.hotmpreset.openinghotm")) + source.sendFeedback(Component.translatable("firmament.hotmpreset.openinghotm")) } } event.subcommand("importhotm") { @@ -205,10 +208,10 @@ object HotmPresets { val template = TemplateUtil.maybeDecodeTemplate<HotmPreset>(SHARE_PREFIX, ClipboardUtils.getTextContents()) if (template == null) { - source.sendFeedback(Text.translatable("firmament.hotmpreset.failedimport")) + source.sendFeedback(Component.translatable("firmament.hotmpreset.failedimport")) } else { highlightedPerks = template.perks.mapTo(mutableSetOf()) { it.perkName } - source.sendFeedback(Text.translatable("firmament.hotmpreset.okayimport")) + source.sendFeedback(Component.translatable("firmament.hotmpreset.okayimport")) MC.sendCommand("hotm") } } diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt new file mode 100644 index 0000000..6c50665 --- /dev/null +++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt @@ -0,0 +1,53 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screens.Screen +import net.minecraft.world.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 = MoulConfigPlatform.wrap(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 94b49f9..23b55e5 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -1,13 +1,17 @@ package moe.nea.firmament.features.mining +import io.github.notenoughupdates.moulconfig.ChromaColour import java.util.regex.Pattern +import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -import net.minecraft.item.ItemStack -import net.minecraft.util.DyeColor -import net.minecraft.util.Hand -import net.minecraft.util.Identifier -import net.minecraft.util.StringIdentifiable +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.components.toasts.SystemToast +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.DyeColor +import net.minecraft.world.InteractionHand +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.StringRepresentable import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.ProcessChatEvent @@ -15,8 +19,6 @@ import moe.nea.firmament.events.ProfileSwitchEvent import moe.nea.firmament.events.SlotClickEvent import moe.nea.firmament.events.UseItemEvent import moe.nea.firmament.events.WorldReadyEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.DurabilityBarEvent import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData @@ -24,6 +26,8 @@ import moe.nea.firmament.util.SHORT_NUMBER_FORMAT import moe.nea.firmament.util.SkyBlockIsland import moe.nea.firmament.util.TIME_PATTERN import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt @@ -33,20 +37,40 @@ import moe.nea.firmament.util.red import moe.nea.firmament.util.render.RenderCircleProgress import moe.nea.firmament.util.render.lerp import moe.nea.firmament.util.skyblock.AbilityUtils +import moe.nea.firmament.util.skyblock.DungeonUtil import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.toShedaniel import moe.nea.firmament.util.tr import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch -object PickaxeAbility : FirmamentFeature { - override val identifier: String +object PickaxeAbility { + val identifier: String get() = "pickaxe-info" + enum class ShowOnTools(val label: String, val items: Set<ItemType>) : StringRepresentable { + ALL("all", ItemType.DRILL, ItemType.PICKAXE, ItemType.SHOVEL, ItemType.AXE), + PICKAXES_AND_DRILLS("pick-and-drill", ItemType.PICKAXE, ItemType.DRILL), + DRILLS("drills", ItemType.DRILL), + ; + override fun getSerializedName(): String? { + return label + } + + constructor(label: String, vararg items: ItemType) : this(label, items.toSet()) + + fun matches(type: ItemType) = items.contains(type) + } + + @Config object TConfig : ManagedConfig(identifier, Category.MINING) { val cooldownEnabled by toggle("ability-cooldown") { false } + val disableInDungeons by toggle("disable-in-dungeons") { true } + val showOnTools by choice("show-on-tools") { ShowOnTools.PICKAXES_AND_DRILLS } val cooldownScale by integer("ability-scale", 16, 64) { 16 } + val cooldownColour by colour("ability-colour") { ChromaColour.fromStaticRGB(187, 54, 44, 128) } + val cooldownReadyToast by toggle("ability-cooldown-toast") { false } val drillFuelBar by toggle("fuel-bar") { true } val blockOnPrivateIsland by choice( "block-on-dynamic", @@ -55,12 +79,12 @@ object PickaxeAbility : FirmamentFeature { } } - enum class BlockPickaxeAbility : StringIdentifiable { + enum class BlockPickaxeAbility : StringRepresentable { NEVER, ALWAYS, ONLY_DESTRUCTIVE; - override fun asString(): String { + override fun getSerializedName(): String { return name } } @@ -79,9 +103,6 @@ object PickaxeAbility : FirmamentFeature { val destructiveAbilities = setOf("Pickobulus") val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET) - override val config: ManagedConfig - get() = TConfig - fun getCooldownPercentage(name: String, cooldown: Duration): Double { val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE val sinceLobbyJoin = lobbyJoinTime.passedTime() @@ -108,9 +129,12 @@ object PickaxeAbility : FirmamentFeature { BlockPickaxeAbility.ONLY_DESTRUCTIVE -> ability.any { it.name in destructiveAbilities } } if (shouldBlock) { - MC.sendChat(tr("firmament.pickaxe.blocked", - "Firmament blocked a pickaxe ability from being used on a private island.") - .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic") + MC.sendChat( + tr( + "firmament.pickaxe.blocked", + "Firmament blocked a pickaxe ability from being used on a private island." + ) + .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic") ) event.cancel() } @@ -140,9 +164,9 @@ object PickaxeAbility : FirmamentFeature { } } ?: return val extra = it.item.extraAttributes - if (!extra.contains("drill_fuel")) return - val fuel = extra.getInt("drill_fuel") - val percentage = fuel / maxFuel.toFloat() + val fuel = extra.getInt("drill_fuel").getOrNull() ?: return + var percentage = fuel / maxFuel.toFloat() + if (percentage > 1f) percentage = 1f it.barOverride = DurabilityBarEvent.DurabilityBar( lerp( DyeColor.RED.toShedaniel(), @@ -156,10 +180,31 @@ 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() + if (!TConfig.cooldownReadyToast) return + val mc: Minecraft = Minecraft.getInstance() + mc.toastManager.addToast( + SystemToast.multiline( + mc, + SystemToast.SystemToastId.NARRATOR_TOGGLE, + tr("firmament.pickaxe.ability-ready", "Pickaxe Cooldown"), + tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!") + ) + ) + } } @Subscribe @@ -179,6 +224,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, @@ -200,21 +246,27 @@ object PickaxeAbility : FirmamentFeature { @Subscribe fun renderHud(event: HudRenderEvent) { if (!TConfig.cooldownEnabled) return + if (TConfig.disableInDungeons && DungeonUtil.isInDungeonIsland) return if (!event.isRenderingCursor) return - var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return - defaultAbilityDurations[ability.name] = ability.cooldown + val stack = MC.player?.getItemInHand(InteractionHand.MAIN_HAND) ?: return + if (!TConfig.showOnTools.matches(ItemType.fromItemStack(stack) ?: ItemType.NIL)) + return + var ability = getCooldownFromLore(stack)?.also { ability -> + defaultAbilityDurations[ability.name] = ability.cooldown + } val ao = abilityOverride - if (ao != ability.name && ao != null) { - ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds) + if (ability == null || (ao != ability.name && ao != null)) { + ability = PickaxeAbilityData(ao ?: return, defaultAbilityDurations[ao] ?: 120.seconds) } - event.context.matrices.push() - event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F) - event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F) + event.context.pose().pushMatrix() + event.context.pose().translate(MC.window.guiScaledWidth / 2F, MC.window.guiScaledHeight / 2F) + event.context.pose().scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat()) RenderCircleProgress.renderCircle( - event.context, Identifier.of("firmament", "textures/gui/circle.png"), + event.context, ResourceLocation.fromNamespaceAndPath("firmament", "textures/gui/circle.png"), getCooldownPercentage(ability.name, ability.cooldown).toFloat(), - 0f, 1f, 0f, 1f + 0f, 1f, 0f, 1f, + color = TConfig.cooldownColour.getEffectiveColourRGB() ) - event.context.matrices.pop() + event.context.pose().popMatrix() } } diff --git a/src/main/kotlin/features/mining/PristineProfitTracker.kt b/src/main/kotlin/features/mining/PristineProfitTracker.kt index 377a470..0470702 100644 --- a/src/main/kotlin/features/mining/PristineProfitTracker.kt +++ b/src/main/kotlin/features/mining/PristineProfitTracker.kt @@ -1,26 +1,26 @@ package moe.nea.firmament.features.mining import io.github.notenoughupdates.moulconfig.xml.Bind -import moe.nea.jarvis.api.Point +import org.joml.Vector2i import kotlinx.serialization.Serializable import kotlinx.serialization.serializer import kotlin.time.Duration.Companion.seconds -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ProcessChatEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.gui.hud.MoulConfigHud import moe.nea.firmament.util.BazaarPriceStrategy import moe.nea.firmament.util.FirmFormatters.formatCommas import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.StringUtil.parseIntWithComma +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder import moe.nea.firmament.util.formattedString import moe.nea.firmament.util.useMatch -object PristineProfitTracker : FirmamentFeature { - override val identifier: String +object PristineProfitTracker { + val identifier: String get() = "pristine-profit" enum class GemstoneKind( @@ -50,14 +50,13 @@ object PristineProfitTracker : FirmamentFeature { var maxCollectionPerSecond: Double = 1.0, ) + @Config object DConfig : ProfileSpecificDataHolder<Data>(serializer(), identifier, ::Data) - override val config: ManagedConfig? - get() = TConfig - + @Config object TConfig : ManagedConfig(identifier, Category.MINING) { val timeout by duration("timeout", 0.seconds, 120.seconds) { 30.seconds } - val gui by position("position", 100, 30) { Point(0.05, 0.2) } + val gui by position("position", 100, 30) { Vector2i() } val useFineGemstones by toggle("fine-gemstones") { false } } @@ -106,28 +105,26 @@ object PristineProfitTracker : FirmamentFeature { val moneyPerSecond = moneyHistogram.averagePer({ it }, 1.seconds) if (collectionPerSecond == null || moneyPerSecond == null) return ProfitHud.collectionCurrent = collectionPerSecond - ProfitHud.collectionText = Text.stringifiedTranslatable("firmament.pristine-profit.collection", + ProfitHud.collectionText = Component.translatableEscape("firmament.pristine-profit.collection", formatCommas(collectionPerSecond * SECONDS_PER_HOUR, 1)).formattedString() ProfitHud.moneyCurrent = moneyPerSecond - ProfitHud.moneyText = Text.stringifiedTranslatable("firmament.pristine-profit.money", + ProfitHud.moneyText = Component.translatableEscape("firmament.pristine-profit.money", formatCommas(moneyPerSecond * SECONDS_PER_HOUR, 1)) .formattedString() val data = DConfig.data - if (data != null) { - if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate() - .passedTime() > 30.seconds - ) { - data.maxCollectionPerSecond = collectionPerSecond - DConfig.markDirty() - } - if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) { - data.maxMoneyPerSecond = moneyPerSecond - DConfig.markDirty() - } - ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond) - ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond) + if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate() + .passedTime() > 30.seconds + ) { + data.maxCollectionPerSecond = collectionPerSecond + DConfig.markDirty() + } + if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) { + data.maxMoneyPerSecond = moneyPerSecond + DConfig.markDirty() } + ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond) + ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond) } diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt new file mode 100644 index 0000000..fe54e6b --- /dev/null +++ b/src/main/kotlin/features/misc/CustomCapes.kt @@ -0,0 +1,172 @@ +package moe.nea.firmament.features.misc + +import util.render.CustomRenderPipelines +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.player.AbstractClientPlayer +import net.minecraft.client.renderer.RenderType +import com.mojang.blaze3d.vertex.VertexConsumer +import net.minecraft.client.renderer.MultiBufferSource +import net.minecraft.client.renderer.entity.state.AvatarRenderState +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.world.entity.player.PlayerSkin +import net.minecraft.core.ClientAsset +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.mc.CustomRenderPassHelper + +object CustomCapes { + val identifier: String + get() = "developer-capes" + + @Config + object TConfig : ManagedConfig(identifier, Category.DEV) { + val showCapes by toggle("show-cape") { true } + } + + interface CustomCapeRenderer { + fun replaceRender( + renderLayer: RenderType, + vertexConsumerProvider: MultiBufferSource, + matrixStack: PoseStack, + model: (VertexConsumer) -> Unit + ) + } + + data class TexturedCapeRenderer( + val location: ResourceLocation + ) : CustomCapeRenderer { + override fun replaceRender( + renderLayer: RenderType, + vertexConsumerProvider: MultiBufferSource, + matrixStack: PoseStack, + model: (VertexConsumer) -> Unit + ) { + model(vertexConsumerProvider.getBuffer(RenderType.entitySolid(location))) + } + } + + data class ParallaxedHighlightCapeRenderer( + val template: ResourceLocation, + val background: ResourceLocation, + val overlay: ResourceLocation, + val animationSpeed: Duration, + ) : CustomCapeRenderer { + override fun replaceRender( + renderLayer: RenderType, + vertexConsumerProvider: MultiBufferSource, + matrixStack: PoseStack, + model: (VertexConsumer) -> Unit + ) { + val animationValue = (startTime.passedTime() / animationSpeed).mod(1F) + CustomRenderPassHelper( + { "Firmament Cape Renderer" }, + renderLayer.mode(), + renderLayer.format(), + MC.instance.mainRenderTarget, + true, + ).use { renderPass -> + renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER) + renderPass.setAllDefaultUniforms() + renderPass.setUniform("Animation", 4) { + it.putFloat(animationValue.toFloat()) + } + renderPass.bindSampler("Sampler0", template) + renderPass.bindSampler("Sampler1", background) + renderPass.bindSampler("Sampler3", overlay) + renderPass.uploadVertices(2048, model) + renderPass.draw() + } + } + } + + interface CapeStorage { + companion object { + @JvmStatic + fun cast(playerEntityRenderState: AvatarRenderState) = + playerEntityRenderState as CapeStorage + + } + + var cape_firmament: CustomCape? + } + + data class CustomCape( + val id: String, + val label: String, + val render: CustomCapeRenderer, + ) + + enum class AllCapes(val label: String, val render: CustomCapeRenderer) { + FIRMAMENT_ANIMATED( + "Animated Firmament", ParallaxedHighlightCapeRenderer( + Firmament.identifier("textures/cape/parallax_template.png"), + Firmament.identifier("textures/cape/parallax_background.png"), + Firmament.identifier("textures/cape/firmament_star.png"), + 110.seconds + ) + ), + UNPLEASANT_GRADIENT( + "unpleasant_gradient", + TexturedCapeRenderer(Firmament.identifier("textures/cape/unpleasant_gradient.png")) + ), + FURFSKY_STATIC( + "FurfSky", + TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png")) + ), + + FIRMAMENT_STATIC( + "Firmament", + TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png")) + ), + HYPIXEL_PLUS( + "Hypixel+", + TexturedCapeRenderer(Firmament.identifier("textures/cape/h_plus.png")) + ), + ; + + val cape = CustomCape(name, label, render) + } + + val byId = AllCapes.entries.associateBy { it.cape.id } + val byUuid = + listOf( + listOf( + Devs.nea to AllCapes.UNPLEASANT_GRADIENT, + Devs.kath to AllCapes.FIRMAMENT_STATIC, + Devs.jani to AllCapes.FIRMAMENT_ANIMATED, + Devs.nat to AllCapes.FIRMAMENT_ANIMATED, + Devs.HPlus.ic22487 to AllCapes.HYPIXEL_PLUS, + ), + Devs.FurfSky.all.map { it to AllCapes.FURFSKY_STATIC }, + ).flatten().flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap() + + @JvmStatic + fun addCapeData( + player: AbstractClientPlayer, + playerEntityRenderState: AvatarRenderState + ) { + if (true) return // TODO: see capefeaturerenderer mixin + val cape = if (TConfig.showCapes) byUuid[player.uuid] else null + val capeStorage = CapeStorage.cast(playerEntityRenderState) + if (cape == null) { + capeStorage.cape_firmament = null + } else { + capeStorage.cape_firmament = cape + playerEntityRenderState.skin = PlayerSkin( + playerEntityRenderState.skin.body, + ClientAsset.ResourceTexture(Firmament.identifier("placeholder/fake_cape"), Firmament.identifier("placeholder/fake_cape")), + playerEntityRenderState.skin.elytra, + playerEntityRenderState.skin.model, + playerEntityRenderState.skin.secure, + ) + playerEntityRenderState.showCape = true + } + } + + val startTime = TimeMark.now() +} diff --git a/src/main/kotlin/features/misc/Devs.kt b/src/main/kotlin/features/misc/Devs.kt new file mode 100644 index 0000000..b9f7e03 --- /dev/null +++ b/src/main/kotlin/features/misc/Devs.kt @@ -0,0 +1,42 @@ +package moe.nea.firmament.features.misc + +import java.util.UUID + +object Devs { + data class Dev( + val uuids: List<UUID>, + ) { + constructor(vararg uuid: UUID) : this(uuid.toList()) + constructor(vararg uuid: String) : this(uuid.map { UUID.fromString(it) }) + } + + val nea = Dev("d3cb85e2-3075-48a1-b213-a9bfb62360c1", "842204e6-6880-487b-ae5a-0595394f9948") + val kath = Dev("add71246-c46e-455c-8345-c129ea6f146c", "b491990d-53fd-4c5f-a61e-19d58cc7eddf") + val jani = Dev("8a9f1841-48e9-48ed-b14f-76a124e6c9df") + val nat = Dev("168300e6-4e74-4a6d-89a0-7b7cf8ad6a7d", "06914e9d-7bc2-4cb7-b112-62c4cc958d96") + + object FurfSky { + val smolegit = Dev("02b38b96-eb19-405a-b319-d6bc00b26ab3") + val itsCen = Dev("ada70b5a-ac37-49d2-b18c-1351672f8051") + val webster = Dev("02166f1b-9e8d-4e48-9e18-ea7a4499492d") + val vrachel = Dev("22e98637-ba97-4b6b-a84f-fb57a461ce43") + val cunuduh = Dev("2a15e3b3-c46e-4718-b907-166e1ab2efdc") + val eiiies = Dev("2ae162f2-81a7-4f91-a4b2-104e78a0a7e1") + val june = Dev("2584a4e3-f917-4493-8ced-618391f3b44f") + val denasu = Dev("313cbd25-8ade-4e41-845c-5cab555a30c9") + val libyKiwii = Dev("4265c52e-bd6f-4d3c-9cf6-bdfc8fb58023") + val madeleaan = Dev("bcb119a3-6000-4324-bda1-744f00c44b31") + val turtleSP = Dev("f1ca1934-a582-4723-8283-89921d008657") + val papayamm = Dev("ae0eea30-f6a2-40fe-ac17-9c80b3423409") + val persuasiveViksy = Dev("ba7ac144-28e0-4f55-a108-1a72fe744c9e") + val all = listOf( + smolegit, itsCen, webster, vrachel, cunuduh, eiiies, + june, denasu, libyKiwii, madeleaan, turtleSP, papayamm, + persuasiveViksy + ) + } + object HPlus { + val ic22487 = Dev("ab2be3b2-bb75-4aaa-892d-9fff5a7e3953") + } + +} diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt new file mode 100644 index 0000000..bbe8044 --- /dev/null +++ b/src/main/kotlin/features/misc/Hud.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.features.misc + +import org.joml.Vector2i +import net.minecraft.client.multiplayer.PlayerInfo +import net.minecraft.network.chat.Component +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.tr + +object Hud { + val identifier: String + get() = "hud" + + @Config + object TConfig : ManagedConfig(identifier, Category.MISC) { + var dayCount by toggle("day-count") { false } + val dayCountHud by position("day-count-hud", 80, 10) { Vector2i() } + var fpsCount by toggle("fps-count") { false } + val fpsCountHud by position("fps-count-hud", 80, 10) { Vector2i() } + var pingCount by toggle("ping-count") { false } + val pingCountHud by position("ping-count-hud", 80, 10) { Vector2i() } + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (TConfig.dayCount) { + it.context.pose().pushMatrix() + TConfig.dayCountHud.applyTransformations(it.context.pose()) + val day = (MC.world?.dayTime ?: 0L) / 24000 + it.context.drawString( + MC.font, + Component.literal(String.format(tr("firmament.config.hud.day-count-hud.display", "Day: %s").string, day)), + 36, + MC.font.lineHeight, + -1, + true + ) + it.context.pose().popMatrix() + } + + if (TConfig.fpsCount) { + it.context.pose().pushMatrix() + TConfig.fpsCountHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, Component.literal( + String.format( + tr("firmament.config.hud.fps-count-hud.display", "FPS: %s").string, MC.instance.fps + ) + ), 36, MC.font.lineHeight, -1, true + ) + it.context.pose().popMatrix() + } + + if (TConfig.pingCount) { + it.context.pose().pushMatrix() + TConfig.pingCountHud.applyTransformations(it.context.pose()) + val ping = MC.player?.let { + val entry: PlayerInfo? = MC.networkHandler?.getPlayerInfo(it.uuid) + entry?.latency ?: -1 + } ?: -1 + it.context.drawString( + MC.font, Component.literal( + String.format( + tr("firmament.config.hud.ping-count-hud.display", "Ping: %s ms").string, ping + ) + ), 36, MC.font.lineHeight, -1, true + ) + + it.context.pose().popMatrix() + } + } +} diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt new file mode 100644 index 0000000..26bec80 --- /dev/null +++ b/src/main/kotlin/features/misc/LicenseViewer.kt @@ -0,0 +1,136 @@ +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 net.minecraft.network.chat.Component +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() = Component.literal(webPresence ?: "<no web presence>") + + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun projectName() = Component.literal(projectName) + + @Bind + fun projectDescription() = Component.literal(projectDescription ?: "<no project description>") + + @get:Bind("developers") + @Transient + val developersO = ObservableList(developers) + + @get:Bind("licenses") + @Transient + val licenses0 = ObservableList(licenses) + } + + @Serializable + data class Developer( + val name: String, + val webPresence: String? = null + ) { + + @Bind("name") + fun nameT() = Component.literal(name) + + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = Component.literal(webPresence ?: "<no web presence>") + } + + @Serializable + data class License( + val licenseName: String, + val licenseUrl: String? = null + ) { + @Bind("name") + fun nameG() = Component.literal(licenseName) + + @Bind + fun open() { + MC.openUrl(licenseUrl ?: return) + } + + @Bind + fun hasUrl() = licenseUrl != null + + @Bind + fun url() = Component.literal(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/misc/ModAnnouncer.kt b/src/main/kotlin/features/misc/ModAnnouncer.kt new file mode 100644 index 0000000..c2aa013 --- /dev/null +++ b/src/main/kotlin/features/misc/ModAnnouncer.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.features.misc + +import io.netty.buffer.ByteBuf +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.JoinServerEvent + +/** + * This is a class that announces all mods to hypixel (or any other server you connect to). + * + * I don't mind cheat mods, i just think they should be honest about themselves. + * + * If you are a cheat mod (or think you could possibly be seen as one), you have two options: + * + * - risk getting your users banned from hypixel by exposing your modid + version number in a packet sent on first connect + * - hide yourself from this list by either a mixin, or (more easily) by specifying a tag in your fabric.mod.json: + * + * ```json + * { + * "schemaVersion": 1, + * "id": "my-cheat-mod", + * "custom": { "firmament:hide_from_modlist": true } + * } + * ``` + */ +object ModAnnouncer { + + data class ModEntry( + val modid: String, + val modVersion: String, + ) { + companion object { + val CODEC: StreamCodec<ByteBuf, ModEntry> = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, ModEntry::modid, + ByteBufCodecs.STRING_UTF8, ModEntry::modVersion, + ::ModEntry + ) + } + } + + data class ModPacket( + val mods: List<ModEntry>, + ) : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type<out ModPacket> { + return ID + } + + companion object { + val ID = CustomPacketPayload.Type<ModPacket>(Firmament.identifier("mod_list")) + val CODEC: StreamCodec<ByteBuf, ModPacket> = ModEntry.CODEC.apply(ByteBufCodecs.list()) + .map(::ModPacket, ModPacket::mods) + } + } + + @Subscribe + fun onServerJoin(event: JoinServerEvent) { + val packet = ModPacket( + FabricLoader.getInstance() + .allMods + .filter { !it.metadata.containsCustomValue("firmament:hide_from_modlist") } + .map { ModEntry(it.metadata.id, it.metadata.version.friendlyString) }) + val pbb = PacketByteBufs.create() + ModPacket.CODEC.encode(pbb, packet) + if (pbb.writerIndex() > ServerboundCustomPayloadPacket.MAX_PAYLOAD_SIZE) + return + + event.networkHandler.send(event.packetSender.createPacket(packet)) + } + + init { + PayloadTypeRegistry.playC2S().register(ModPacket.ID, ModPacket.CODEC) + } +} diff --git a/src/main/kotlin/features/notifications/Notifications.kt b/src/main/kotlin/features/notifications/Notifications.kt deleted file mode 100644 index 8d912d1..0000000 --- a/src/main/kotlin/features/notifications/Notifications.kt +++ /dev/null @@ -1,7 +0,0 @@ - -package moe.nea.firmament.features.notifications - -import moe.nea.firmament.features.FirmamentFeature - -object Notifications { -} diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt new file mode 100644 index 0000000..3597d6d --- /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.network.chat.Component +import net.minecraft.core.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 + .filter { it.x != null && it.y != null && it.z != null } + .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) -> Component, + ) { + 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) -> Component + ) { + val text = ClipboardUtils.getTextContents() + val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ZERO) } + 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.ZERO) { + tr("firmament.command.waypoint.export.cw", + "Copied $it waypoints to clipboard in ColeWeight format.") + } + } + } + thenLiteral("exportrelativecw") { + thenExecute { + copyAndInform(source, MC.player?.blockPosition() ?: BlockPos.ZERO) { + 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) { + tr("firmament.command.waypoint.import.cw.success", + "Imported $it waypoints from ColeWeight.") + } + } + } + thenLiteral("importrelativecw") { + thenExecute { + importAndInform(source, MC.player!!.blockPosition()) { + 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/FairySouls.kt b/src/main/kotlin/features/world/FairySouls.kt index 1263074..b073726 100644 --- a/src/main/kotlin/features/world/FairySouls.kt +++ b/src/main/kotlin/features/world/FairySouls.kt @@ -1,131 +1,123 @@ - - package moe.nea.firmament.features.world import io.github.moulberry.repo.data.Coordinate +import me.shedaniel.math.Color import kotlinx.serialization.Serializable import kotlinx.serialization.serializer -import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Vec3d import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.SkyblockServerUpdateEvent import moe.nea.firmament.events.WorldRenderLastEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SkyBlockIsland import moe.nea.firmament.util.blockPos +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.data.ProfileSpecificDataHolder -import moe.nea.firmament.util.render.RenderInWorldContext import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld import moe.nea.firmament.util.unformattedString -object FairySouls : FirmamentFeature { - - - @Serializable - data class Data( - val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf() - ) - - override val config: ManagedConfig - get() = TConfig - - object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data) - - - object TConfig : ManagedConfig("fairy-souls", Category.MISC) { - val displaySouls by toggle("show") { false } - val resetSouls by button("reset") { - DConfig.data?.foundSouls?.clear() != null - updateMissingSouls() - } - } - - - override val identifier: String get() = "fairy-souls" - - val playerReach = 5 - val playerReachSquared = playerReach * playerReach - - var currentLocationName: SkyBlockIsland? = null - var currentLocationSouls: List<Coordinate> = emptyList() - var currentMissingSouls: List<Coordinate> = emptyList() - - fun updateMissingSouls() { - currentMissingSouls = emptyList() - val c = DConfig.data ?: return - val fi = c.foundSouls[currentLocationName] ?: setOf() - val cms = currentLocationSouls.toMutableList() - fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) } - currentMissingSouls = cms - } - - fun updateWorldSouls() { - currentLocationSouls = emptyList() - val loc = currentLocationName ?: return - currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return - } - - fun findNearestClickableSoul(): Coordinate? { - val player = MC.player ?: return null - val pos = player.pos - val location = SBData.skyblockLocation ?: return null - val soulLocations: List<Coordinate> = - RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null - return soulLocations - .map { it to it.blockPos.getSquaredDistance(pos) } - .filter { it.second < playerReachSquared } - .minByOrNull { it.second } - ?.first - } - - private fun markNearestSoul() { - val nearestSoul = findNearestClickableSoul() ?: return - val c = DConfig.data ?: return - val loc = currentLocationName ?: return - val idx = currentLocationSouls.indexOf(nearestSoul) - c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx) - DConfig.markDirty() - updateMissingSouls() - } - - @Subscribe - fun onWorldRender(it: WorldRenderLastEvent) { - if (!TConfig.displaySouls) return - renderInWorld(it) { - currentMissingSouls.forEach { - block(it.blockPos, 0x80FFFF00.toInt()) - } - color(1f, 0f, 1f, 1f) - currentLocationSouls.forEach { - wireframeCube(it.blockPos) - } - } - } - - @Subscribe - fun onProcessChat(it: ProcessChatEvent) { - when (it.text.unformattedString) { - "You have already found that Fairy Soul!" -> { - markNearestSoul() - } - - "SOUL! You found a Fairy Soul!" -> { - markNearestSoul() - } - } - } - - @Subscribe - fun onLocationChange(it: SkyblockServerUpdateEvent) { - currentLocationName = it.newLocraw?.skyblockLocation - updateWorldSouls() - updateMissingSouls() - } +object FairySouls { + + + @Serializable + data class Data( + val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf() + ) + + @Config + object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data) + + @Config + object TConfig : ManagedConfig("fairy-souls", Category.MISC) { + val displaySouls by toggle("show") { false } + val resetSouls by button("reset") { + DConfig.data?.foundSouls?.clear() != null + updateMissingSouls() + } + } + + + val identifier: String get() = "fairy-souls" + + val playerReach = 5 + val playerReachSquared = playerReach * playerReach + + var currentLocationName: SkyBlockIsland? = null + var currentLocationSouls: List<Coordinate> = emptyList() + var currentMissingSouls: List<Coordinate> = emptyList() + + fun updateMissingSouls() { + currentMissingSouls = emptyList() + val c = DConfig.data ?: return + val fi = c.foundSouls[currentLocationName] ?: setOf() + val cms = currentLocationSouls.toMutableList() + fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) } + currentMissingSouls = cms + } + + fun updateWorldSouls() { + currentLocationSouls = emptyList() + val loc = currentLocationName ?: return + currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return + } + + fun findNearestClickableSoul(): Coordinate? { + val player = MC.player ?: return null + val pos = player.position + val location = SBData.skyblockLocation ?: return null + val soulLocations: List<Coordinate> = + RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null + return soulLocations + .map { it to it.blockPos.distToCenterSqr(pos) } + .filter { it.second < playerReachSquared } + .minByOrNull { it.second } + ?.first + } + + private fun markNearestSoul() { + val nearestSoul = findNearestClickableSoul() ?: return + val c = DConfig.data ?: return + val loc = currentLocationName ?: return + val idx = currentLocationSouls.indexOf(nearestSoul) + c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx) + DConfig.markDirty() + updateMissingSouls() + } + + @Subscribe + fun onWorldRender(it: WorldRenderLastEvent) { + if (!TConfig.displaySouls) return + renderInWorld(it) { + currentMissingSouls.forEach { + block(it.blockPos, Color.ofRGBA(176, 0, 255, 128).color) + } + currentLocationSouls.forEach { + wireframeCube(it.blockPos) + } + } + } + + @Subscribe + fun onProcessChat(it: ProcessChatEvent) { + when (it.text.unformattedString) { + "You have already found that Fairy Soul!" -> { + markNearestSoul() + } + + "SOUL! You found a Fairy Soul!" -> { + markNearestSoul() + } + } + } + + @Subscribe + fun onLocationChange(it: SkyblockServerUpdateEvent) { + currentLocationName = it.newLocraw?.skyblockLocation + updateWorldSouls() + updateMissingSouls() + } } diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt new file mode 100644 index 0000000..c6e2610 --- /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.network.chat.Component +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.DataHolder +import moe.nea.firmament.util.tr + +object FirmWaypointManager { + object DConfig : DataHolder<MutableMap<String, FirmWaypoints>>(serializer(), "waypoints", ::mutableMapOf) + + 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: (Component) -> Unit) { + val copy = waypoints.deepCopy() + if (copy.isRelativeTo != null) { + val origin = MC.player!!.blockPosition() + copy.waypoints.replaceAll { + it.copy( + x = it.x + origin.x, + y = it.y + origin.y, + z = it.z + origin.z, + ) + } + copy.lastRelativeImport = origin.immutable() + 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!!.blockPosition() + 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 { DConfig.data.keys } + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + waypoints.id = get(name) + val exportableWaypoints = createExportableCopy(waypoints) + DConfig.data[get(name)] = exportableWaypoints + DConfig.markDirty() + 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 { DConfig.data.keys } + thenExecute { + val name = get(name) + val waypoints = DConfig.data[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..0b018cb --- /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.core.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/NavigableWaypoint.kt b/src/main/kotlin/features/world/NavigableWaypoint.kt index 28a517f..653fd87 100644 --- a/src/main/kotlin/features/world/NavigableWaypoint.kt +++ b/src/main/kotlin/features/world/NavigableWaypoint.kt @@ -1,7 +1,7 @@ package moe.nea.firmament.features.world import io.github.moulberry.repo.data.NEUItem -import net.minecraft.util.math.BlockPos +import net.minecraft.core.BlockPos import moe.nea.firmament.util.SkyBlockIsland abstract class NavigableWaypoint { diff --git a/src/main/kotlin/features/world/NavigationHelper.kt b/src/main/kotlin/features/world/NavigationHelper.kt index acdfb86..ea886bf 100644 --- a/src/main/kotlin/features/world/NavigationHelper.kt +++ b/src/main/kotlin/features/world/NavigationHelper.kt @@ -1,10 +1,10 @@ package moe.nea.firmament.features.world import io.github.moulberry.repo.constants.Islands -import net.minecraft.text.Text -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Position -import net.minecraft.util.math.Vec3i +import net.minecraft.network.chat.Component +import net.minecraft.core.BlockPos +import net.minecraft.core.Position +import net.minecraft.core.Vec3i import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.SkyblockServerUpdateEvent import moe.nea.firmament.events.TickEvent @@ -72,7 +72,7 @@ object NavigationHelper { fun onMovement(event: TickEvent) { // TODO: add a movement tick event maybe? val tp = targetWaypoint ?: return val p = MC.player ?: return - if (p.squaredDistanceTo(tp.position.toCenterPos()) < 5 * 5) { + if (p.distanceToSqr(tp.position.center) < 5 * 5) { targetWaypoint = null } } @@ -84,11 +84,11 @@ object NavigationHelper { RenderInWorldContext.renderInWorld(event) { if (nt != null) { waypoint(nt.blockPos, - Text.literal("Teleporter to " + nt.toIsland.userFriendlyName), - Text.literal("(towards " + tp.name + "§f)")) + Component.literal("Teleporter to " + nt.toIsland.userFriendlyName), + Component.literal("(towards " + tp.name + "§f)")) } else if (tp.island == SBData.skyblockLocation) { waypoint(tp.position, - Text.literal(tp.name)) + Component.literal(tp.name)) } } } @@ -96,7 +96,7 @@ object NavigationHelper { fun tryWarpNear() { val tp = targetWaypoint if (tp == null) { - MC.sendChat(Text.literal("Could not find a waypoint to warp you to. Select one first.")) + MC.sendChat(Component.literal("Could not find a waypoint to warp you to. Select one first.")) return } WarpUtil.teleportToNearestWarp(tp.island, tp.position.asPositionView()) @@ -106,15 +106,15 @@ object NavigationHelper { fun Vec3i.asPositionView(): Position { return object : Position { - override fun getX(): Double { + override fun x(): Double { return this@asPositionView.x.toDouble() } - override fun getY(): Double { + override fun y(): Double { return this@asPositionView.y.toDouble() } - override fun getZ(): Double { + override fun z(): Double { return this@asPositionView.z.toDouble() } } diff --git a/src/main/kotlin/features/world/NpcWaypointGui.kt b/src/main/kotlin/features/world/NpcWaypointGui.kt index 6146e50..3bae3e8 100644 --- a/src/main/kotlin/features/world/NpcWaypointGui.kt +++ b/src/main/kotlin/features/world/NpcWaypointGui.kt @@ -2,6 +2,7 @@ package moe.nea.firmament.features.world import io.github.notenoughupdates.moulconfig.observer.ObservableList import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.network.chat.Component import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures.atOnce import moe.nea.firmament.keybindings.SavedKeyBinding @@ -11,7 +12,7 @@ class NpcWaypointGui( data class NavigableWaypointW(val waypoint: NavigableWaypoint) { @Bind - fun name() = waypoint.name + fun name() = Component.literal(waypoint.name) @Bind fun isSelected() = NavigationHelper.targetWaypoint == waypoint diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt new file mode 100644 index 0000000..ac1600e --- /dev/null +++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt @@ -0,0 +1,72 @@ +package moe.nea.firmament.features.world + +import me.shedaniel.math.Color +import kotlin.time.Duration.Companion.seconds +import net.minecraft.network.chat.Component +import net.minecraft.core.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, Color.ofRGBA(255, 255, 0, 128).color) + } + temporaryPlayerWaypointList.forEach { (player, waypoint) -> + val skin = + MC.networkHandler?.listedOnlinePlayers?.find { it.profile.name == player }?.skin?.body + withFacingThePlayer(waypoint.pos.center) { + waypoint(waypoint.pos, Component.translatableEscape("firmament.waypoint.temporary", player)) + if (skin != null) { + matrixStack.translate(0F, -20F, 0F) + // Head front + texture( + skin.id(), 16, 16, // TODO: 1.21.10 test + 1 / 8f, 1 / 8f, + 2 / 8f, 2 / 8f, + ) + // Head overlay + texture( + skin.id(), 16, 16, // TODO: 1.21.10 + 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..9097d3a 100644 --- a/src/main/kotlin/features/world/Waypoints.kt +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -2,150 +2,129 @@ 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 net.minecraft.commands.arguments.coordinates.BlockPosArgument +import net.minecraft.network.chat.Component +import net.minecraft.world.phys.Vec3 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.data.Config +import moe.nea.firmament.util.data.ManagedConfig +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 +object Waypoints { + val identifier: String get() = "waypoints" + @Config object TConfig : ManagedConfig(identifier, Category.MINING) { // TODO: add to misc 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, Color.ofRGBA(0, 80, 160, 128).color) + if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.center) { + text(Component.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), - ) + tracer(w.waypoints[orderedIndex].blockPos.center, color = firstColor.color, 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, col.color) - if (TConfig.showIndex) - withFacingThePlayer(pos.toCenterPos()) { - text(Text.literal(index.toString())) - } + ).reversed().forEach { (waypoint, col) -> + val (index, pos) = waypoint + block(pos.blockPos, col.color) + if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.center) { + text(Component.literal(index.toString())) } + } } } } @Subscribe fun onTick(event: TickEvent) { - if (waypoints.isEmpty() || !ordered) return - orderedIndex %= waypoints.size - val p = MC.player?.pos ?: return + val w = useNonEmptyWaypoints() ?: return + if (!w.isOrdered) return + orderedIndex %= w.waypoints.size + val p = MC.player?.position ?: return if (TConfig.skipToNearest) { orderedIndex = - (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size + (w.waypoints.withIndex().minBy { it.value.blockPos.distToCenterSqr(p) }.index + 1) % w.waypoints.size + } else { - if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) { - orderedIndex = (orderedIndex + 1) % waypoints.size + if (w.waypoints[orderedIndex].blockPos.closerToCenterThan(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 } } @Subscribe fun onCommand(event: CommandEvent.SubCommand) { event.subcommand("waypoint") { - thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> + thenArgument("pos", BlockPosArgument.blockPos()) { pos -> thenExecute { - val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) - waypoints.add(position) + source + val position = pos.get(this).getBlockPos(source.asFakeServer()) + val w = useEditableWaypoints() + w.waypoints.add(FirmWaypoints.Waypoint.from(position)) source.sendFeedback( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.command.waypoint.added", position.x, position.y, @@ -155,31 +134,75 @@ object Waypoints : FirmamentFeature { } } } - 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() - source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) + waypoints = null + source.sendFeedback(Component.translatable("firmament.command.waypoint.clear")) } } thenLiteral("toggleordered") { thenExecute { - ordered = !ordered - if (ordered) { - val p = MC.player?.pos ?: Vec3d.ZERO - orderedIndex = - waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0 + val w = useEditableWaypoints() + w.isOrdered = !w.isOrdered + if (w.isOrdered) { + val p = MC.player?.position ?: Vec3.ZERO + orderedIndex = // TODO: this should be extracted to a utility method + w.waypoints.withIndex().minByOrNull { it.value.blockPos.distToCenterSqr(p) }?.index ?: 0 } - source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered")) + source.sendFeedback(Component.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}")) } } thenLiteral("skip") { thenExecute { - if (ordered && waypoints.isNotEmpty()) { - orderedIndex = (orderedIndex + 1) % waypoints.size - source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) + val w = useNonEmptyWaypoints() + if (w != null && w.isOrdered) { + orderedIndex = (orderedIndex + 1) % w.size + source.sendFeedback(Component.translatable("firmament.command.waypoint.skip")) } else { - source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) + source.sendError(Component.translatable("firmament.command.waypoint.skip.error")) } } } @@ -187,79 +210,35 @@ 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( + Component.translatableEscape( + "firmament.command.waypoint.remove", + index + ) + ) } else { - source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) + source.sendError(Component.translatableEscape("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, - ) - } - } - } - } - } - - @Subscribe - fun onWorldReady(event: WorldReadyEvent) { - temporaryPlayerWaypointList.clear() - } + fun textInvalidIndex(index: Int) = + tr( + "firmament.command.waypoint.invalid-index", + "Invalid index $index provided." + ) + + fun textNothingToExport(): Component = + 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 +251,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 - ) -} |
