aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/firmament
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-07-22 03:08:56 +0200
committernea <nea@nea.moe>2023-07-22 03:08:56 +0200
commitcdf3938b778188211ad316d16381e0d8c7beac75 (patch)
treeeb3508942f61bee290b0558c97c3e04946070b0a /src/main/kotlin/moe/nea/firmament
parent538827af3bfccbc4cee1ff2e9cb76922108ace9e (diff)
downloadFirmament-cdf3938b778188211ad316d16381e0d8c7beac75.tar.gz
Firmament-cdf3938b778188211ad316d16381e0d8c7beac75.tar.bz2
Firmament-cdf3938b778188211ad316d16381e0d8c7beac75.zip
Add image preview
Diffstat (limited to 'src/main/kotlin/moe/nea/firmament')
-rw-r--r--src/main/kotlin/moe/nea/firmament/Firmament.kt24
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/ClientChatLineReceivedEvent.kt1
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/ScreenRenderPostEvent.kt14
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt150
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/MC.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/textutil.kt21
7 files changed, 205 insertions, 9 deletions
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<ClientChatLineReceivedEvent>()
}
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<ScreenRenderPostEvent>()
+}
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<FeatureManager.Config>(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<String, Deferred<Image?>> =
+ Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
+
+ 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))
+ }
+ }
+}