aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/chat
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/features/chat
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadfirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/features/chat')
-rw-r--r--src/main/kotlin/features/chat/AutoCompletions.kt57
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt161
-rw-r--r--src/main/kotlin/features/chat/QuickCommands.kt100
3 files changed, 318 insertions, 0 deletions
diff --git a/src/main/kotlin/features/chat/AutoCompletions.kt b/src/main/kotlin/features/chat/AutoCompletions.kt
new file mode 100644
index 0000000..9144898
--- /dev/null
+++ b/src/main/kotlin/features/chat/AutoCompletions.kt
@@ -0,0 +1,57 @@
+
+
+package moe.nea.firmament.features.chat
+
+import com.mojang.brigadier.arguments.StringArgumentType.string
+import moe.nea.firmament.annotations.Subscribe
+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.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
+
+object AutoCompletions : FirmamentFeature {
+
+ object TConfig : ManagedConfig(identifier) {
+ val provideWarpTabCompletion by toggle("warp-complete") { true }
+ val replaceWarpIsByWarpIsland by toggle("warp-is") { true }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+ override val identifier: String
+ get() = "auto-completions"
+
+ @Subscribe
+ fun onMaskCommands(event: MaskCommands) {
+ if (TConfig.provideWarpTabCompletion) {
+ event.mask("warp")
+ }
+ }
+
+ @Subscribe
+ fun onCommandEvent(event: CommandEvent) {
+ if (!TConfig.provideWarpTabCompletion) return
+ event.deleteCommand("warp")
+ event.register("warp") {
+ thenArgument("to", string()) { toArg ->
+ suggestsList {
+ RepoManager.neuRepo.constants?.islands?.warps?.flatMap { listOf(it.warp) + it.aliases } ?: listOf()
+ }
+ thenExecute {
+ val warpName = get(toArg)
+ if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) {
+ MC.sendServerCommand("warp island")
+ } else {
+ MC.sendServerCommand("warp $warpName")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt
new file mode 100644
index 0000000..f2cb78a
--- /dev/null
+++ b/src/main/kotlin/features/chat/ChatLinks.kt
@@ -0,0 +1,161 @@
+
+
+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.util.Collections
+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.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 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.util.MC
+import moe.nea.firmament.util.transformEachRecursively
+import moe.nea.firmament.util.unformattedString
+
+object ChatLinks : FirmamentFeature {
+ override val identifier: String
+ get() = "chat-links"
+
+ object TConfig : ManagedConfig(identifier) {
+ 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 isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
+
+ override val config get() = TConfig
+ val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".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?>>())
+
+ 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
+ }
+ }
+ }
+
+ 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
+ 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
+ }
+ }
+}
diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt
new file mode 100644
index 0000000..5944b92
--- /dev/null
+++ b/src/main/kotlin/features/chat/QuickCommands.kt
@@ -0,0 +1,100 @@
+
+
+package moe.nea.firmament.features.chat
+
+import com.mojang.brigadier.context.CommandContext
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.RestArgumentType
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+
+object QuickCommands : FirmamentFeature {
+ override val identifier: String
+ get() = "quick-commands"
+
+ fun removePartialPrefix(text: String, prefix: String): String? {
+ var lf: String? = null
+ for (i in 1..prefix.length) {
+ if (text.startsWith(prefix.substring(0, i))) {
+ lf = text.substring(i)
+ }
+ }
+ return lf
+ }
+
+ val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
+ val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
+
+ @Subscribe
+ fun onCommands(it: CommandEvent) {
+ it.register("join") {
+ thenArgument("what", RestArgumentType) { what ->
+ thenExecute {
+ val what = this[what]
+ if (!SBData.isOnSkyblock) {
+ MC.sendCommand("join $what")
+ return@thenExecute
+ }
+ val joinName = getNameForFloor(what.replace(" ", "").lowercase())
+ if (joinName == null) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
+ } else {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
+ joinName))
+ MC.sendCommand("joininstance $joinName")
+ }
+ }
+ }
+ thenExecute {
+ source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
+ }
+ }
+ }
+
+ fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
+ val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
+ if (kuudraLevel != null) {
+ val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
+ it.startsWith(
+ kuudraLevel,
+ true
+ )
+ }
+ if (l !in kuudraLevelNames.indices) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
+ kuudraLevel))
+ return null
+ }
+ return "KUUDRA_${kuudraLevelNames[l]}"
+ }
+ val masterLevel = removePartialPrefix(w, "master")
+ val normalLevel =
+ removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
+ val dungeonLevel = masterLevel ?: normalLevel
+ if (dungeonLevel != null) {
+ val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
+ it.startsWith(
+ dungeonLevel,
+ true
+ )
+ }
+ if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
+ return "CATACOMBS_ENTRANCE"
+ }
+ if (l !in dungeonLevelNames.indices) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
+ kuudraLevel))
+ return null
+ }
+ return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
+ }
+ return null
+ }
+}