WIP: Port to compilation on 1.21.4
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
package moe.nea.firmament.features.chat
|
||||
|
||||
import io.ktor.client.request.get
|
||||
@@ -7,16 +5,15 @@ import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import java.net.URL
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import moe.nea.jarvis.api.Point
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlin.math.min
|
||||
import net.minecraft.client.gui.screen.ChatScreen
|
||||
import net.minecraft.client.render.RenderLayer
|
||||
import net.minecraft.client.texture.NativeImage
|
||||
import net.minecraft.client.texture.NativeImageBackedTexture
|
||||
import net.minecraft.scoreboard.ScoreboardCriterion.RenderType
|
||||
import net.minecraft.text.ClickEvent
|
||||
import net.minecraft.text.HoverEvent
|
||||
import net.minecraft.text.Style
|
||||
@@ -35,130 +32,132 @@ import moe.nea.firmament.util.transformEachRecursively
|
||||
import moe.nea.firmament.util.unformattedString
|
||||
|
||||
object ChatLinks : FirmamentFeature {
|
||||
override val identifier: String
|
||||
get() = "chat-links"
|
||||
override val identifier: String
|
||||
get() = "chat-links"
|
||||
|
||||
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) }
|
||||
}
|
||||
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) }
|
||||
}
|
||||
|
||||
private fun isHostAllowed(host: String) =
|
||||
TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) }
|
||||
private fun isHostAllowed(host: String) =
|
||||
TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) }
|
||||
|
||||
private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
|
||||
private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
|
||||
|
||||
override val config get() = TConfig
|
||||
val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex()
|
||||
override val config get() = TConfig
|
||||
val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex()
|
||||
val nextTexId = AtomicInteger(0)
|
||||
|
||||
data class Image(
|
||||
val texture: Identifier,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
)
|
||||
data class Image(
|
||||
val texture: Identifier,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
)
|
||||
|
||||
val imageCache: MutableMap<String, Deferred<Image?>> =
|
||||
Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
|
||||
val imageCache: MutableMap<String, Deferred<Image?>> =
|
||||
Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
|
||||
|
||||
private 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
|
||||
}
|
||||
}
|
||||
}
|
||||
private 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 texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
|
||||
MC.textureManager.registerTexture(
|
||||
texId,
|
||||
NativeImageBackedTexture(image)
|
||||
)
|
||||
Image(texId, image.width, image.height)
|
||||
} else
|
||||
null
|
||||
} catch (exc: Exception) {
|
||||
exc.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val imageExtensions = listOf("jpg", "png", "gif", "jpeg")
|
||||
fun isImageUrl(url: String): Boolean {
|
||||
return (url.substringAfterLast('.').lowercase() in imageExtensions)
|
||||
}
|
||||
val imageExtensions = listOf("jpg", "png", "gif", "jpeg")
|
||||
fun isImageUrl(url: String): Boolean {
|
||||
return (url.substringAfterLast('.').lowercase() in imageExtensions)
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun onRender(it: ScreenRenderPostEvent) {
|
||||
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
|
||||
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()
|
||||
val pos = TConfig.position
|
||||
pos.applyTransformations(it.drawContext.matrices)
|
||||
val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width))
|
||||
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()
|
||||
}
|
||||
@Subscribe
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun onRender(it: ScreenRenderPostEvent) {
|
||||
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
|
||||
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()
|
||||
val pos = TConfig.position
|
||||
pos.applyTransformations(it.drawContext.matrices)
|
||||
val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width))
|
||||
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()
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun onModifyChat(it: ModifyChatEvent) {
|
||||
if (!TConfig.enableLinks) return
|
||||
it.replaceWith = it.replaceWith.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)))
|
||||
.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url))
|
||||
)
|
||||
)
|
||||
if (isImageUrl(url))
|
||||
tryCacheUrl(url)
|
||||
index = range.last + 1
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
@Subscribe
|
||||
fun onModifyChat(it: ModifyChatEvent) {
|
||||
if (!TConfig.enableLinks) return
|
||||
it.replaceWith = it.replaceWith.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)))
|
||||
.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url))
|
||||
)
|
||||
)
|
||||
if (isImageUrl(url))
|
||||
tryCacheUrl(url)
|
||||
index = range.last + 1
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user