From cdf3938b778188211ad316d16381e0d8c7beac75 Mon Sep 17 00:00:00 2001 From: nea Date: Sat, 22 Jul 2023 03:08:56 +0200 Subject: Add image preview --- .../moe/nea/firmament/mixins/MixinChatHud.java | 16 ++- .../moe/nea/firmament/mixins/MixinChatScreen.java | 8 ++ src/main/kotlin/moe/nea/firmament/Firmament.kt | 24 ++-- .../events/ClientChatLineReceivedEvent.kt | 1 + .../nea/firmament/events/ScreenRenderPostEvent.kt | 14 ++ .../moe/nea/firmament/features/FeatureManager.kt | 2 + .../nea/firmament/features/chat/ImagePreview.kt | 150 +++++++++++++++++++++ src/main/kotlin/moe/nea/firmament/util/MC.kt | 2 + src/main/kotlin/moe/nea/firmament/util/textutil.kt | 21 +++ .../resources/assets/firmament/lang/en_us.json | 7 +- 10 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/MixinChatScreen.java create mode 100644 src/main/kotlin/moe/nea/firmament/events/ScreenRenderPostEvent.kt create mode 100644 src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt (limited to 'src/main') diff --git a/src/main/java/moe/nea/firmament/mixins/MixinChatHud.java b/src/main/java/moe/nea/firmament/mixins/MixinChatHud.java index f0db847..13921e5 100644 --- a/src/main/java/moe/nea/firmament/mixins/MixinChatHud.java +++ b/src/main/java/moe/nea/firmament/mixins/MixinChatHud.java @@ -8,14 +8,22 @@ import net.minecraft.text.Text; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ChatHud.class) public class MixinChatHud { - @Inject(at = @At("HEAD"), method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", cancellable = true) - public void onAddMessage(Text message, MessageSignatureData signature, int ticks, MessageIndicator indicator, boolean refresh, CallbackInfo ci) { - if (ClientChatLineReceivedEvent.Companion.publish(new ClientChatLineReceivedEvent(message)).getCancelled()) { - ci.cancel(); + @ModifyArg(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V"), method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V") + public Text onAddMessage(Text message) { + var event = new ClientChatLineReceivedEvent(message); + if (ClientChatLineReceivedEvent.Companion.publish(event).getCancelled()) { + return null; } + return event.getReplaceWith(); + } + + @Inject(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", at = @At("HEAD"), cancellable = true) + public void onAddMessage2(Text message, MessageSignatureData signature, int ticks, MessageIndicator indicator, boolean refresh, CallbackInfo ci) { + if (message == null) ci.cancel(); } } diff --git a/src/main/java/moe/nea/firmament/mixins/MixinChatScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinChatScreen.java new file mode 100644 index 0000000..4d75c44 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MixinChatScreen.java @@ -0,0 +1,8 @@ +package moe.nea.firmament.mixins; + +import net.minecraft.client.gui.screen.ChatScreen; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(ChatScreen.class) +public class MixinChatScreen { +} diff --git a/src/main/kotlin/moe/nea/firmament/Firmament.kt b/src/main/kotlin/moe/nea/firmament/Firmament.kt index 46329f1..6564e9b 100644 --- a/src/main/kotlin/moe/nea/firmament/Firmament.kt +++ b/src/main/kotlin/moe/nea/firmament/Firmament.kt @@ -20,19 +20,19 @@ package moe.nea.firmament import com.mojang.brigadier.CommandDispatcher import dev.architectury.event.events.client.ClientTickEvent -import io.ktor.client.HttpClient -import io.ktor.client.plugins.UserAgent -import io.ktor.client.plugins.cache.HttpCache -import io.ktor.client.plugins.compression.ContentEncoding -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logging -import io.ktor.serialization.kotlinx.json.json +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.compression.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.serialization.kotlinx.json.* import java.nio.file.Files import java.nio.file.Path import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.Version import net.fabricmc.loader.api.metadata.ModMetadata @@ -51,6 +51,7 @@ import net.minecraft.command.CommandRegistryAccess import net.minecraft.util.Identifier import moe.nea.firmament.commands.registerFirmamentCommand import moe.nea.firmament.dbus.FirmamentDbusObject +import moe.nea.firmament.events.ScreenRenderPostEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.features.FeatureManager import moe.nea.firmament.repo.HypixelStaticData @@ -134,7 +135,12 @@ object Firmament { globalJob.cancel() } }) - + ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight -> + ScreenEvents.afterRender(screen) + .register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta -> + ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext)) + }) + }) } fun identifier(path: String) = Identifier(MOD_ID, path) diff --git a/src/main/kotlin/moe/nea/firmament/events/ClientChatLineReceivedEvent.kt b/src/main/kotlin/moe/nea/firmament/events/ClientChatLineReceivedEvent.kt index 604422d..7af411d 100644 --- a/src/main/kotlin/moe/nea/firmament/events/ClientChatLineReceivedEvent.kt +++ b/src/main/kotlin/moe/nea/firmament/events/ClientChatLineReceivedEvent.kt @@ -9,6 +9,7 @@ import moe.nea.firmament.util.unformattedString */ data class ClientChatLineReceivedEvent(val text: Text) : FirmamentEvent.Cancellable() { val unformattedString = text.unformattedString + var replaceWith: Text = text companion object : FirmamentEventBus() } diff --git a/src/main/kotlin/moe/nea/firmament/events/ScreenRenderPostEvent.kt b/src/main/kotlin/moe/nea/firmament/events/ScreenRenderPostEvent.kt new file mode 100644 index 0000000..2d061a2 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/events/ScreenRenderPostEvent.kt @@ -0,0 +1,14 @@ +package moe.nea.firmament.events + +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen + +data class ScreenRenderPostEvent( + val screen: Screen, + val mouseX: Int, + val mouseY: Int, + val tickDelta: Float, + val drawContext: DrawContext +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt index 0f0a166..86db2f0 100644 --- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt +++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt @@ -21,6 +21,7 @@ package moe.nea.firmament.features import kotlinx.serialization.Serializable import kotlinx.serialization.serializer import moe.nea.firmament.Firmament +import moe.nea.firmament.features.chat.ImagePreview import moe.nea.firmament.features.debug.DebugView import moe.nea.firmament.features.debug.DeveloperFeatures import moe.nea.firmament.features.fishing.FishingWarning @@ -55,6 +56,7 @@ object FeatureManager : DataHolder(serializer(), "feature loadFeature(SlotLocking) loadFeature(StorageOverlay) loadFeature(CraftingOverlay) + loadFeature(ImagePreview) loadFeature(SaveCursorPosition) if (Firmament.DEBUG) { loadFeature(DeveloperFeatures) diff --git a/src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt b/src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt new file mode 100644 index 0000000..5dcacc3 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt @@ -0,0 +1,150 @@ +package moe.nea.firmament.features.chat + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.utils.io.jvm.javaio.* +import java.net.URL +import java.util.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +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.HoverEvent +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.events.ClientChatLineReceivedEvent +import moe.nea.firmament.events.ScreenRenderPostEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.transformEachRecursively +import moe.nea.firmament.util.unformattedString + +object ImagePreview : FirmamentFeature { + override val identifier: String + get() = "image-preview" + + object TConfig : ManagedConfig(identifier) { + val enabled by toggle("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 screenPercentage by integer("percentage", 10, 100) { 50 } + } + + fun isHostAllowed(host: String) = + TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) } + + fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) + + override val config get() = TConfig + val urlRegex = "https://[^. ]+\\.[^ ]+(\\.(png|gif|jpe?g))(\\?[^ ]*)?( |$)".toRegex() + + data class Image( + val texture: Identifier, + val width: Int, + val height: Int, + ) + + val imageCache: MutableMap> = + Collections.synchronizedMap(mutableMapOf>()) + + fun tryCacheUrl(url: String) { + if (!isUrlAllowed(url)) { + return + } + if (url in imageCache) { + return + } + 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 texture = MC.textureManager.registerDynamicTexture( + "dynamic_image_preview", + NativeImageBackedTexture(image) + ) + Image(texture, image.width, image.height) + } else + null + } catch (exc: Exception) { + exc.printStackTrace() + null + } + } + } + + override fun onLoad() { + ClientChatLineReceivedEvent.subscribe { + it.replaceWith = it.text.transformEachRecursively { child -> + val text = child.string + if ("://" !in text) return@transformEachRecursively child + val s = Text.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))) + break + } + val range = nextMatch.groups[0]!!.range + val url = nextMatch.groupValues[0] + s.append(Text.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))) + ) + ) + tryCacheUrl(url) + index = range.last + 1 + } + s + } + } + + ScreenRenderPostEvent.subscribe { + if (!TConfig.enabled) return@subscribe + if (it.screen !is ChatScreen) return@subscribe + val hoveredComponent = + MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return@subscribe + val hoverEvent = hoveredComponent.hoverEvent ?: return@subscribe + val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return@subscribe + val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return@subscribe + val imageFuture = imageCache[url] ?: return@subscribe + if (!imageFuture.isCompleted) return@subscribe + val image = imageFuture.getCompleted() ?: return@subscribe + val screen = MC.screen!! + val scale = + min( + 1F, + min( + (TConfig.screenPercentage / 100F * screen.width.toFloat()) / image.width, + screen.height.toFloat() / image.height + ) + ) + it.drawContext.matrices.push() + it.drawContext.matrices.scale(scale, scale, 1F) + it.drawContext.drawTexture( + image.texture, + 0, + 0, + 1F, + 1F, + image.width, + image.height, + image.width, + image.height, + ) + it.drawContext.matrices.pop() + } + } +} diff --git a/src/main/kotlin/moe/nea/firmament/util/MC.kt b/src/main/kotlin/moe/nea/firmament/util/MC.kt index d595b61..e6e53fa 100644 --- a/src/main/kotlin/moe/nea/firmament/util/MC.kt +++ b/src/main/kotlin/moe/nea/firmament/util/MC.kt @@ -28,6 +28,8 @@ object MC { player?.networkHandler?.sendCommand(command) } + inline val textureManager get() = MinecraftClient.getInstance().textureManager + inline val inGameHud get() = MinecraftClient.getInstance().inGameHud inline val font get() = MinecraftClient.getInstance().textRenderer inline val soundManager get() = MinecraftClient.getInstance().soundManager inline val player get() = MinecraftClient.getInstance().player diff --git a/src/main/kotlin/moe/nea/firmament/util/textutil.kt b/src/main/kotlin/moe/nea/firmament/util/textutil.kt index 3096282..5c443c3 100644 --- a/src/main/kotlin/moe/nea/firmament/util/textutil.kt +++ b/src/main/kotlin/moe/nea/firmament/util/textutil.kt @@ -21,6 +21,7 @@ package moe.nea.firmament.util import net.minecraft.text.LiteralTextContent import net.minecraft.text.Text import net.minecraft.text.TextContent +import net.minecraft.text.TranslatableTextContent import moe.nea.firmament.Firmament @@ -86,3 +87,23 @@ class TextMatcher(text: Text) { val Text.unformattedString get() = string.replace("ยง.".toRegex(), "") + +fun Text.transformEachRecursively(function: (Text) -> Text): Text { + val c = this.content + if (c is TranslatableTextContent) { + return Text.translatableWithFallback(c.key, c.fallback, *c.args.map { + (if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function) + }.toTypedArray()).also { new -> + new.style = this.style + new.siblings.clear() + this.siblings.forEach { child -> + new.siblings.add(child.transformEachRecursively(function)) + } + } + } + return function(this.copy().also { it.siblings.clear() }).also { tt -> + this.siblings.forEach { + tt.siblings.add(it.transformEachRecursively(function)) + } + } +} diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json index f75fea0..dfd2fa3 100644 --- a/src/main/resources/assets/firmament/lang/en_us.json +++ b/src/main/resources/assets/firmament/lang/en_us.json @@ -66,5 +66,10 @@ "firmament.config.storage-overlay.padding": "Padding", "firmament.config.storage-overlay.scroll-speed": "Scroll Speed", "firmament.config.storage-overlay.inverse-scroll": "Invert Scroll", - "firmament.config.storage-overlay.margin": "Margin" + "firmament.config.storage-overlay.margin": "Margin", + "firmament.config.image-preview": "Image Preview", + "firmament.config.image-preview.enabled": "Enable Image Preview", + "firmament.config.image-preview.allow-all-hosts": "Allow all Image Hosts", + "firmament.config.image-preview.allowed-hosts": "Allowed Image Hosts", + "firmament.config.image-preview.percentage": "Image Width (Percentage of screen)" } -- cgit