aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/org/polyfrost/chatting
diff options
context:
space:
mode:
authorWyvest <wyvestbusiness@gmail.com>2023-11-22 08:18:19 +0900
committerWyvest <wyvestbusiness@gmail.com>2023-11-22 08:18:19 +0900
commit8b373f577d9c6dde26357ef3fc86691f1efef9b4 (patch)
treea5328e995d8f4df21a9fe94ac8e384be08833c70 /src/main/kotlin/org/polyfrost/chatting
parent64230799777473246b5f98efbc596206c5bbf42d (diff)
downloadChatting-8b373f577d9c6dde26357ef3fc86691f1efef9b4.tar.gz
Chatting-8b373f577d9c6dde26357ef3fc86691f1efef9b4.tar.bz2
Chatting-8b373f577d9c6dde26357ef3fc86691f1efef9b4.zip
update PGT and relocate to org.polyfrost
Diffstat (limited to 'src/main/kotlin/org/polyfrost/chatting')
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/Chatting.kt272
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatRegexes.kt11
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatScrollingHook.kt5
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatSearchingManager.kt42
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatShortcuts.kt79
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatSpamBlock.kt124
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatTab.kt112
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatTabs.kt354
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/chat/ChatTabsJson.kt15
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/command/ChattingCommand.kt14
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/config/ChattingConfig.kt313
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/gui/components/CleanButton.kt103
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/gui/components/ClearButton.kt42
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/gui/components/RenderType.kt7
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/gui/components/ScreenshotButton.kt36
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/gui/components/SearchButton.kt70
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/gui/components/TabButton.kt46
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/utils/EaseOutQuart.kt7
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/utils/ModCompatHooks.kt100
-rw-r--r--src/main/kotlin/org/polyfrost/chatting/utils/RenderUtils.kt259
20 files changed, 2011 insertions, 0 deletions
diff --git a/src/main/kotlin/org/polyfrost/chatting/Chatting.kt b/src/main/kotlin/org/polyfrost/chatting/Chatting.kt
new file mode 100644
index 0000000..0e8745c
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/Chatting.kt
@@ -0,0 +1,272 @@
+package org.polyfrost.chatting
+
+import cc.polyfrost.oneconfig.libs.universal.UDesktop
+import cc.polyfrost.oneconfig.libs.universal.UMinecraft
+import cc.polyfrost.oneconfig.libs.universal.UResolution
+import cc.polyfrost.oneconfig.utils.Notifications
+import cc.polyfrost.oneconfig.utils.commands.CommandManager
+import cc.polyfrost.oneconfig.utils.dsl.browseLink
+import org.polyfrost.chatting.chat.ChatSearchingManager
+import org.polyfrost.chatting.chat.ChatShortcuts
+import org.polyfrost.chatting.chat.ChatSpamBlock
+import org.polyfrost.chatting.chat.ChatTabs
+import org.polyfrost.chatting.command.ChattingCommand
+import org.polyfrost.chatting.config.ChattingConfig
+import org.polyfrost.chatting.utils.ModCompatHooks
+import org.polyfrost.chatting.utils.copyToClipboard
+import org.polyfrost.chatting.utils.createBindFramebuffer
+import org.polyfrost.chatting.utils.screenshot
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.*
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.client.renderer.OpenGlHelper
+import net.minecraft.client.settings.KeyBinding
+import net.minecraft.client.shader.Framebuffer
+import net.minecraft.util.MathHelper
+import net.minecraftforge.client.event.RenderGameOverlayEvent
+import net.minecraftforge.common.MinecraftForge.EVENT_BUS
+import net.minecraftforge.fml.client.registry.ClientRegistry
+import net.minecraftforge.fml.common.Loader
+import net.minecraftforge.fml.common.Mod
+import net.minecraftforge.fml.common.event.FMLInitializationEvent
+import net.minecraftforge.fml.common.event.FMLLoadCompleteEvent
+import net.minecraftforge.fml.common.event.FMLPostInitializationEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import org.lwjgl.input.Keyboard
+import org.polyfrost.chatting.hook.ChatLineHook
+import org.polyfrost.chatting.mixin.GuiNewChatAccessor
+import java.awt.image.BufferedImage
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+@Mod(
+ modid = Chatting.ID,
+ name = Chatting.NAME,
+ version = Chatting.VER,
+ modLanguageAdapter = "cc.polyfrost.oneconfig.utils.KotlinLanguageAdapter"
+)
+object Chatting {
+
+ val keybind = KeyBinding("Screenshot Chat", Keyboard.KEY_NONE, "Chatting")
+ const val NAME = "@NAME@"
+ const val VER = "@VER@"
+ const val ID = "@ID@"
+ var doTheThing = false
+ var isPatcher = false
+ private set
+ var isBetterChat = false
+ private set
+ var isSkytils = false
+ private set
+ var isHychat = false
+ private set
+
+ private var time = -1L
+ var deltaTime = 17L
+
+ private val fileFormatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd_HH.mm.ss'.png'")
+
+ val oldModDir = File(File(Minecraft.getMinecraft().mcDataDir, "W-OVERFLOW"), NAME)
+
+ @Mod.EventHandler
+ fun onInitialization(event: FMLInitializationEvent) {
+ ChattingConfig
+ CommandManager.INSTANCE.registerCommand(ChattingCommand())
+ ClientRegistry.registerKeyBinding(keybind)
+ EVENT_BUS.register(this)
+ EVENT_BUS.register(ChatSpamBlock)
+ ChatTabs.initialize()
+ ChatShortcuts.initialize()
+ }
+
+ @Mod.EventHandler
+ fun onPostInitialization(event: FMLPostInitializationEvent) {
+ isPatcher = Loader.isModLoaded("patcher")
+ isBetterChat = Loader.isModLoaded("betterchat")
+ isSkytils = Loader.isModLoaded("skytils")
+ isHychat = Loader.isModLoaded("hychat")
+ }
+
+ @Mod.EventHandler
+ fun onForgeLoad(event: FMLLoadCompleteEvent) {
+ if (ChattingConfig.informForAlternatives) {
+ if (isHychat) {
+ Notifications.INSTANCE.send(
+ NAME,
+ "Hychat can be removed as it is replaced by Chatting. Click here for more information.",
+ Runnable {
+ UDesktop.browseLink("https://microcontrollersdev.github.io/Alternatives/1.8.9/hychat")
+ })
+ }
+ if (isSkytils) {
+ try {
+ skytilsCompat(Class.forName("gg.skytils.skytilsmod.core.Config"))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ try {
+ skytilsCompat(Class.forName("skytils.skytilsmod.core.Config"))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+ if (isBetterChat) {
+ Notifications.INSTANCE.send(
+ NAME,
+ "BetterChat can be removed as it is replaced by Chatting. Click here to open your mods folder to delete the BetterChat file.",
+ Runnable {
+ UDesktop.open(File("./mods"))
+ })
+ }
+ }
+ }
+
+ private fun skytilsCompat(skytilsClass: Class<*>) {
+ val instance = skytilsClass.getDeclaredField("INSTANCE")
+ val chatTabs = skytilsClass.getDeclaredField("chatTabs")
+ chatTabs.isAccessible = true
+ if (chatTabs.getBoolean(instance)) {
+ Notifications.INSTANCE.send(
+ NAME,
+ "Skytils' chat tabs can be disabled as it is replace by Chatting.\nClick here to automatically do this.",
+ Runnable {
+ chatTabs.setBoolean(instance, false)
+ ChattingConfig.chatTabs = true
+ ChattingConfig.hypixelOnlyChatTabs = true
+ ChattingConfig.save()
+ skytilsClass.getMethod("markDirty").invoke(instance)
+ skytilsClass.getMethod("writeData").invoke(instance)
+ })
+ }
+ val copyChat = skytilsClass.getDeclaredField("copyChat")
+ copyChat.isAccessible = true
+ if (copyChat.getBoolean(instance)) {
+ Notifications.INSTANCE.send(
+ NAME,
+ "Skytils' copy chat messages can be disabled as it is replace by Chatting.\nClick here to automatically do this.",
+ Runnable {
+ copyChat.setBoolean(instance, false)
+ skytilsClass.getMethod("markDirty").invoke(instance)
+ skytilsClass.getMethod("writeData").invoke(instance)
+ })
+ }
+ }
+
+ @SubscribeEvent
+ fun onRenderTick(event: RenderGameOverlayEvent.Pre) {
+ if (event.type == RenderGameOverlayEvent.ElementType.ALL) {
+ if (time == -1L) {
+ time = UMinecraft.getTime()
+ } else {
+ val currentTime = UMinecraft.getTime()
+ deltaTime = currentTime - time
+ time = currentTime
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onTickEvent(event: TickEvent.ClientTickEvent) {
+ if (event.phase == TickEvent.Phase.START && Minecraft.getMinecraft().theWorld != null && Minecraft.getMinecraft().thePlayer != null && (Minecraft.getMinecraft().currentScreen == null || Minecraft.getMinecraft().currentScreen is GuiChat)) {
+ if (doTheThing) {
+ screenshotChat()
+ doTheThing = false
+ }
+ }
+ }
+
+ fun getChatHeight(opened: Boolean): Int {
+ var height = if (opened) ChattingConfig.focusedHeight else ChattingConfig.unfocusedHeight
+ height = (height * Minecraft.getMinecraft().gameSettings.chatScale).toInt()
+ val chatY = ModCompatHooks.yOffset + ModCompatHooks.chatPosition
+ if (height + chatY + 27 > (UResolution.scaledHeight / Minecraft.getMinecraft().gameSettings.chatScale).toInt() - 27 - chatY) {
+ height = (UResolution.scaledHeight / Minecraft.getMinecraft().gameSettings.chatScale).toInt() - 27 - chatY
+ }
+ return height
+ }
+
+ fun screenshotLine(line: ChatLine): BufferedImage? {
+ val hud = Minecraft.getMinecraft().ingameGUI
+ val chat = hud.chatGUI
+ val i = MathHelper.floor_float(chat.chatWidth / chat.chatScale)
+ return screenshot(
+ hashMapOf<ChatLine, String>().also {
+ GuiUtilRenderComponents.splitText(
+ line.chatComponent,
+ i,
+ Minecraft.getMinecraft().fontRendererObj,
+ false,
+ false
+ ).map { it.formattedText }.reversed().forEach { string ->
+ it[line] = string
+ }
+ }
+ )
+ }
+
+ private fun screenshotChat() {
+ screenshotChat(0)
+ }
+
+ fun screenshotChat(scrollPos: Int) {
+ val hud = Minecraft.getMinecraft().ingameGUI
+ val chat = hud.chatGUI
+ val chatLines = LinkedHashMap<ChatLine, String>()
+ ChatSearchingManager.filterMessages(
+ ChatSearchingManager.lastSearch,
+ (chat as GuiNewChatAccessor).drawnChatLines
+ )?.let { drawnLines ->
+ val chatHeight =
+ if (ChattingConfig.customChatHeight) getChatHeight(true) / 9 else GuiNewChat.calculateChatboxHeight(
+ Minecraft.getMinecraft().gameSettings.chatHeightFocused / 9
+ )
+ for (i in scrollPos until drawnLines.size.coerceAtMost(scrollPos + chatHeight)) {
+ chatLines[drawnLines[i]] = drawnLines[i].chatComponent.formattedText
+ }
+
+ screenshot(chatLines)?.copyToClipboard()
+ }
+ }
+
+ private fun screenshot(messages: HashMap<ChatLine, String>): BufferedImage? {
+ if (messages.isEmpty()) {
+ Notifications.INSTANCE.send("Chatting", "Chat window is empty.")
+ return null
+ }
+ if (!OpenGlHelper.isFramebufferEnabled()) {
+ Notifications.INSTANCE.send(
+ "Chatting",
+ "Screenshot failed, please disable “Fast Render” in OptiFine’s “Performance” tab."
+ )
+ return null
+ }
+
+ val fr: FontRenderer = ModCompatHooks.fontRenderer
+ val width = messages.maxOf { fr.getStringWidth(it.value) + (if (ChattingConfig.showChatHeads && ((it.key as ChatLineHook).hasDetected() || ChattingConfig.offsetNonPlayerMessages)) 10 else 0) } + 4
+ val fb: Framebuffer = createBindFramebuffer(width * 2, (messages.size * 9) * 2)
+ val file = File(Minecraft.getMinecraft().mcDataDir, "screenshots/chat/" + fileFormatter.format(Date()))
+
+ GlStateManager.scale(2f, 2f, 1f)
+ val scale = Minecraft.getMinecraft().gameSettings.chatScale
+ GlStateManager.scale(scale, scale, 1f)
+ messages.entries.forEachIndexed { i: Int, entry: MutableMap.MutableEntry<ChatLine, String> ->
+ ModCompatHooks.redirectDrawString(entry.value, 0f, (messages.size - 1 - i) * 9f, 0xffffff, entry.key, true)
+ }
+
+ val image = fb.screenshot(file)
+ Minecraft.getMinecraft().entityRenderer.setupOverlayRendering()
+ Minecraft.getMinecraft().framebuffer.bindFramebuffer(true)
+ Notifications.INSTANCE.send(
+ "Chatting",
+ "Chat screenshotted successfully." + (if (ChattingConfig.copyMode != 1) "\nClick to open." else ""),
+ Runnable {
+ if (!UDesktop.open(file)) {
+ Notifications.INSTANCE.send("Chatting", "Could not browse!")
+ }
+ })
+ return image
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatRegexes.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatRegexes.kt
new file mode 100644
index 0000000..0d6909e
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatRegexes.kt
@@ -0,0 +1,11 @@
+package org.polyfrost.chatting.chat
+
+data class ChatRegexes(val regexList: List<String>?) {
+ val compiledRegexList: MutableList<Regex> = arrayListOf()
+
+ init {
+ regexList?.forEach {
+ compiledRegexList.add(Regex(it))
+ }
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatScrollingHook.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatScrollingHook.kt
new file mode 100644
index 0000000..982329a
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatScrollingHook.kt
@@ -0,0 +1,5 @@
+package org.polyfrost.chatting.chat
+
+object ChatScrollingHook {
+ var shouldSmooth = false
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatSearchingManager.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatSearchingManager.kt
new file mode 100644
index 0000000..d20a358
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatSearchingManager.kt
@@ -0,0 +1,42 @@
+package org.polyfrost.chatting.chat
+
+import cc.polyfrost.oneconfig.libs.caffeine.cache.Cache
+import cc.polyfrost.oneconfig.libs.caffeine.cache.Caffeine
+import cc.polyfrost.oneconfig.libs.universal.wrappers.message.UTextComponent
+import net.minecraft.client.gui.ChatLine
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+object ChatSearchingManager {
+ private var counter: AtomicInteger = AtomicInteger(0)
+ private var POOL: ThreadPoolExecutor = ThreadPoolExecutor(
+ 50, 50,
+ 0L, TimeUnit.SECONDS,
+ LinkedBlockingQueue()
+ ) { r ->
+ Thread(
+ r,
+ "Chat Filter Cache Thread ${counter.incrementAndGet()}"
+ )
+ }
+
+ @JvmStatic
+ val cache: Cache<String, List<ChatLine>> = Caffeine.newBuilder().executor(POOL).maximumSize(5000).build()
+
+ var lastSearch = ""
+
+ @JvmStatic
+ fun filterMessages(text: String, list: List<ChatLine>): List<ChatLine>? {
+ if (text.isBlank()) return list
+ val cached = cache.getIfPresent(text)
+ return cached ?: run {
+ cache.put(text, list.filter {
+ UTextComponent.stripFormatting(it.chatComponent.unformattedText).lowercase()
+ .contains(text.lowercase())
+ })
+ cache.getIfPresent(text)
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatShortcuts.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatShortcuts.kt
new file mode 100644
index 0000000..0c85553
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatShortcuts.kt
@@ -0,0 +1,79 @@
+package org.polyfrost.chatting.chat
+
+import cc.polyfrost.oneconfig.config.core.ConfigUtils
+import org.polyfrost.chatting.Chatting
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import java.io.File
+
+object ChatShortcuts {
+ private val oldShortcutsFile = File(Chatting.oldModDir, "chatshortcuts.json")
+ private val shortcutsFile = ConfigUtils.getProfileFile("chatshortcuts.json")
+ private val PARSER = JsonParser()
+
+ private var initialized = false
+
+ val shortcuts = object : ArrayList<Pair<String, String>>() {
+ private val comparator = Comparator<Pair<String, String>> { o1, o2 ->
+ return@Comparator o2.first.length.compareTo(o1.first.length)
+ }
+
+ override fun add(element: Pair<String, String>): Boolean {
+ val value = super.add(element)
+ sortWith(comparator)
+ return value
+ }
+ }
+
+ fun initialize() {
+ if (initialized) {
+ return
+ } else {
+ initialized = true
+ }
+ if (shortcutsFile.exists()) {
+ try {
+ val jsonObj = PARSER.parse(shortcutsFile.readText()).asJsonObject
+ for (shortcut in jsonObj.entrySet()) {
+ shortcuts.add(shortcut.key to shortcut.value.asString)
+ }
+ return
+ } catch (_: Throwable) {
+ shortcutsFile.renameTo(File(shortcutsFile.parentFile, "chatshortcuts.json.bak"))
+ }
+ }
+ shortcutsFile.createNewFile()
+ if (oldShortcutsFile.exists()) {
+ shortcutsFile.writeText(
+ oldShortcutsFile.readText()
+ )
+ } else {
+ shortcutsFile.writeText(JsonObject().toString())
+ }
+ }
+
+ fun removeShortcut(key: String) {
+ shortcuts.removeIf { it.first == key }
+ val jsonObj = PARSER.parse(shortcutsFile.readText()).asJsonObject
+ jsonObj.remove(key)
+ shortcutsFile.writeText(jsonObj.toString())
+ }
+
+ fun writeShortcut(key: String, value: String) {
+ shortcuts.add(key to value)
+ val jsonObj = PARSER.parse(shortcutsFile.readText()).asJsonObject
+ jsonObj.addProperty(key, value)
+ shortcutsFile.writeText(jsonObj.toString())
+ }
+
+ fun handleSentCommand(command: String): String {
+ shortcuts.forEach {
+ if (command == it.first || (command.startsWith(it.first) && command.substringAfter(it.first)
+ .startsWith(" "))
+ ) {
+ return command.replaceFirst(it.first, it.second)
+ }
+ }
+ return command
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatSpamBlock.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatSpamBlock.kt
new file mode 100644
index 0000000..da5dde8
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatSpamBlock.kt
@@ -0,0 +1,124 @@
+package org.polyfrost.chatting.chat
+
+import org.polyfrost.chatting.config.ChattingConfig
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import java.text.Normalizer
+import net.minecraft.util.ChatComponentText
+import net.minecraft.util.EnumChatFormatting
+import net.minecraftforge.client.event.ClientChatReceivedEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+object ChatSpamBlock {
+ /*
+ Made by @KTibow
+ Based off of Unspam (also by @KTibow)
+ Algorithm based off of https://paulgraham.com/spam.html
+ */
+ private val PLAYER_MESSAGE = Regex("^(\\[VIP\\+?\\] |\\[MVP\\+?\\+?\\] |)(\\w{2,16}): (.*)$")
+
+ @SubscribeEvent
+ fun onChat(event: ClientChatReceivedEvent) {
+ val message = event.message.unformattedText.replace(Regex("\u00A7."), "")
+ if (!PLAYER_MESSAGE.matches(message)) return
+
+ val (rank, player, content) = PLAYER_MESSAGE.matchEntire(message)!!.destructured
+
+ if (ChattingConfig.spamThreshold != 100) {
+ val tokens = tokenize(content)
+ val spamProb = findSpamProbability(tokens)
+ if (spamProb * 100 > ChattingConfig.spamThreshold) {
+ if (ChattingConfig.hideSpam) {
+ event.isCanceled = true
+ } else {
+ var newMessage =
+ EnumChatFormatting.DARK_GRAY.toString() +
+ EnumChatFormatting.STRIKETHROUGH.toString()
+ if (!ChattingConfig.customChatFormatting) {
+ newMessage += rank
+ }
+ newMessage += "$player${EnumChatFormatting.DARK_GRAY}: $content"
+ event.message = ChatComponentText(newMessage)
+ }
+ return
+ }
+ }
+ if (ChattingConfig.customChatFormatting) {
+ val coloredPlayer = findRankColor(rank) + player + EnumChatFormatting.RESET.toString()
+ event.message = ChatComponentText("$coloredPlayer: $content")
+ }
+ }
+
+ private fun tokenize(message: String): MutableList<String> {
+ val strippedMessage =
+ Normalizer.normalize(message, Normalizer.Form.NFKC)
+ .replace(Regex("[^\\w\\s/]"), " ")
+ .lowercase()
+ .trim()
+ val tokens = strippedMessage.split(Regex("\\s+")).toMutableList()
+ if (tokens.size <= 2) {
+ tokens.add("TINY_LENGTH")
+ } else if (tokens.size <= 4) {
+ tokens.add("SMALL_LENGTH")
+ } else if (tokens.size <= 7) {
+ tokens.add("MEDIUM_LENGTH")
+ } else {
+ tokens.add("LONG_LENGTH")
+ }
+ if (message.replace(Regex("[\\w\\s]"), "").length > 2) {
+ tokens.add("SPECIAL_CHARS")
+ } else if (message.replace(Regex("[\\w\\s]"), "").isNotEmpty()) {
+ tokens.add("SPECIAL_CHAR")
+ } else {
+ tokens.add("LOW_SPECIAL_CHARS")
+ }
+ if (message.replace(Regex("[^A-Z]"), "").length >= message.length / 4) {
+ tokens.add("HIGH_CAPS")
+ } else {
+ tokens.add("LOW_CAPS")
+ }
+ return tokens
+ }
+
+ private fun findSpamProbability(tokens: MutableList<String>): Double {
+ val tokenProbs = mutableMapOf<String, Double>()
+ for (token in tokens) {
+ if (!spamInfoJson.has(token)) continue
+ val spamInToken = spamInfoJson.get(token).asJsonObject.get("spam").asDouble
+ val fineInToken = spamInfoJson.get(token).asJsonObject.get("fine").asDouble
+ tokenProbs[token] =
+ ((spamInToken / messageCountsJson.get("spam").asInt) /
+ (fineInToken / messageCountsJson.get("fine").asInt +
+ spamInToken / messageCountsJson.get("spam").asInt))
+ }
+ val spamProbs = tokenProbs.values.toMutableList()
+ val fineProbs = tokenProbs.values.map { 1 - it }.toMutableList()
+ val spamProbability = spamProbs.reduce { a, b -> a * b }
+ val fineProbability = fineProbs.reduce { a, b -> a * b }
+ return spamProbability / (spamProbability + fineProbability)
+ }
+
+ private fun findRankColor(rank: String): String {
+ println(rank)
+ return when (rank) {
+ "[VIP] ",
+ "[VIP+] " -> EnumChatFormatting.GREEN.toString()
+ "[MVP] ",
+ "[MVP+] " -> EnumChatFormatting.AQUA.toString()
+ "[MVP++] " -> EnumChatFormatting.GOLD.toString()
+ else -> EnumChatFormatting.GRAY.toString()
+ }
+ }
+
+ private fun getResourceAsText(path: String): String? =
+ object {}.javaClass.getResource(path)?.readText()
+ private val spamInfoJson: JsonObject
+ private val messageCountsJson: JsonObject
+
+ init {
+ // Load the file spamInfo.json from resources/
+ val spamInfo = getResourceAsText("/spamInfo.json")
+ spamInfoJson = JsonParser().parse(spamInfo).asJsonObject
+ messageCountsJson = JsonParser().parse(" { \"fine\": 668, \"spam\": 230 }").asJsonObject
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatTab.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatTab.kt
new file mode 100644
index 0000000..bd65f11
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatTab.kt
@@ -0,0 +1,112 @@
+package org.polyfrost.chatting.chat
+
+import org.polyfrost.chatting.gui.components.TabButton
+import com.google.gson.annotations.SerializedName
+import net.minecraft.client.Minecraft
+import net.minecraft.util.EnumChatFormatting
+import net.minecraft.util.IChatComponent
+import java.util.*
+
+data class ChatTab(
+ val enabled: Boolean,
+ val name: String,
+ val unformatted: Boolean,
+ val lowercase: Boolean?,
+ @SerializedName("starts") val startsWith: List<String>?,
+ val contains: List<String>?,
+ @SerializedName("ends") val endsWith: List<String>?,
+ val equals: List<String>?,
+ @SerializedName("regex") val uncompiledRegex: List<String>?,
+ @SerializedName("ignore_starts") val ignoreStartsWith: List<String>?,
+ @SerializedName("ignore_contains") val ignoreContains: List<String>?,
+ @SerializedName("ignore_ends") val ignoreEndsWith: List<String>?,
+ @SerializedName("ignore_equals") val ignoreEquals: List<String>?,
+ @SerializedName("ignore_regex") val uncompiledIgnoreRegex: List<String>?,
+ val color: Int?,
+ @SerializedName("hovered_color") val hoveredColor: Int?,
+ @SerializedName("selected_color") val selectedColor: Int?,
+ val prefix: String?,
+) {
+ lateinit var button: TabButton
+ lateinit var compiledRegex: ChatRegexes
+ lateinit var compiledIgnoreRegex: ChatRegexes
+
+ //Ugly hack to make GSON not make button / regex null
+ fun initialize() {
+ compiledRegex = ChatRegexes(uncompiledRegex)
+ compiledIgnoreRegex = ChatRegexes(uncompiledIgnoreRegex)
+ val width = Minecraft.getMinecraft().fontRendererObj.getStringWidth(name)
+ button = TabButton(653452, run {
+ val returnValue = x - 2
+ x += 6 + width
+ return@run returnValue
+ }, width + 4, 12, this)
+ }
+
+ fun shouldRender(chatComponent: IChatComponent): Boolean {
+ val message =
+ (if (unformatted) EnumChatFormatting.getTextWithoutFormattingCodes(chatComponent.unformattedText) else chatComponent.formattedText).let {
+ if (lowercase == true) it.lowercase(
+ Locale.ENGLISH
+ ) else it
+ }
+ ignoreStartsWith?.forEach {
+ if (message.startsWith(it)) {
+ return false
+ }
+ }
+ ignoreEquals?.forEach {
+ if (message == it) {
+ return false
+ }
+ }
+ ignoreEndsWith?.forEach {
+ if (message.endsWith(it)) {
+ return false
+ }
+ }
+ ignoreContains?.forEach {
+ if (message.contains(it)) {
+ return false
+ }
+ }
+ compiledIgnoreRegex.compiledRegexList.forEach {
+ if (it.matches(message)) {
+ return false
+ }
+ }
+ if (startsWith.isNullOrEmpty() && equals.isNullOrEmpty() && endsWith.isNullOrEmpty() && contains.isNullOrEmpty() && uncompiledRegex.isNullOrEmpty()) {
+ return true
+ }
+ equals?.forEach {
+ if (message == it) {
+ return true
+ }
+ }
+ startsWith?.forEach {
+ if (message.startsWith(it)) {
+ return true
+ }
+ }
+ endsWith?.forEach {
+ if (message.endsWith(it)) {
+ return true
+ }
+ }
+ contains?.forEach {
+ if (message.contains(it)) {
+ return true
+ }
+ }
+ compiledRegex.compiledRegexList.forEach {
+ if (it.matches(message)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ companion object {
+ private var x = 4
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatTabs.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatTabs.kt
new file mode 100644
index 0000000..b46f55d
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatTabs.kt
@@ -0,0 +1,354 @@
+package org.polyfrost.chatting.chat
+
+import cc.polyfrost.oneconfig.config.core.ConfigUtils
+import org.polyfrost.chatting.Chatting
+import org.polyfrost.chatting.gui.components.TabButton
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import com.google.gson.JsonPrimitive
+import net.minecraft.client.Minecraft
+import net.minecraft.util.IChatComponent
+import java.io.File
+
+object ChatTabs {
+ private val GSON = GsonBuilder().setPrettyPrinting().create()
+ private val PARSER = JsonParser()
+ val tabs = arrayListOf<ChatTab>()
+ var currentTabs: ArrayList<ChatTab?> = object : ArrayList<ChatTab?>() {
+ override fun add(element: ChatTab?): Boolean {
+ if (element == null) return false
+ val returnValue = super.add(element)
+ if (Minecraft.getMinecraft().theWorld != null && returnValue) {
+ Minecraft.getMinecraft().ingameGUI.chatGUI.refreshChat()
+ }
+ return returnValue
+ }
+ }
+ var hasCancelledAnimation = false
+ private var initialized = false
+
+ private val tabFile = ConfigUtils.getProfileFile("chattabs.json")
+ private val oldTabFile = File(Chatting.oldModDir, "chattabs.json")
+
+ fun initialize() {
+ if (initialized) {
+ return
+ } else {
+ initialized = true
+ }
+ if (!tabFile.exists()) {
+ if (oldTabFile.exists()) {
+ tabFile.writeText(oldTabFile.readText())
+ handleFile()
+ } else {
+ generateNewFile()
+ }
+ } else {
+ handleFile()
+ }
+ tabs.forEach {
+ it.initialize()
+ }
+ currentTabs.clear()
+ currentTabs.add(tabs[0])
+ }
+
+ private fun handleFile() {
+ try {
+ val chatTabJson = GSON.fromJson(tabFile.readText(), ChatTabsJson::class.java)
+ when (chatTabJson.version) {
+ 1 -> {
+ // ver 2 adds `enabled`
+ chatTabJson.tabs.forEach {
+ applyVersion2Changes(it.asJsonObject)
+ applyVersion3Changes(it.asJsonObject)
+ applyVersion4Changes(it.asJsonObject)
+ applyVersion5Changes(it.asJsonObject)
+ applyVersion6Changes(it.asJsonObject)
+ }
+ chatTabJson.version = ChatTabsJson.VERSION
+ tabFile.writeText(GSON.toJson(chatTabJson))
+ }
+ 2 -> {
+ // ver 3 adds ignore_
+ chatTabJson.tabs.forEach {
+ applyVersion3Changes(it.asJsonObject)
+ applyVersion4Changes(it.asJsonObject)
+ applyVersion5Changes(it.asJsonObject)
+ applyVersion6Changes(it.asJsonObject)
+ }
+ chatTabJson.version = ChatTabsJson.VERSION
+ tabFile.writeText(GSON.toJson(chatTabJson))
+ }
+ 3 -> {
+ // ver 4 adds color options
+ chatTabJson.tabs.forEach {
+ applyVersion4Changes(it.asJsonObject)
+ applyVersion5Changes(it.asJsonObject)
+ applyVersion6Changes(it.asJsonObject)
+ }
+ chatTabJson.version = ChatTabsJson.VERSION
+ tabFile.writeText(GSON.toJson(chatTabJson))
+ }
+ 4 -> {
+ // ver 5 adds lowercase
+ chatTabJson.tabs.forEach {
+ applyVersion5Changes(it.asJsonObject)
+ applyVersion6Changes(it.asJsonObject)
+ }
+ chatTabJson.version = ChatTabsJson.VERSION
+ tabFile.writeText(GSON.toJson(chatTabJson))
+ }
+ 5 -> {
+ // ver 6 changes pm regex
+ chatTabJson.tabs.forEach {
+ applyVersion6Changes(it.asJsonObject)
+ }
+ chatTabJson.version = ChatTabsJson.VERSION
+ tabFile.writeText(GSON.toJson(chatTabJson))
+ }
+ }
+ chatTabJson.tabs.forEach {
+ val chatTab = GSON.fromJson(it.toString(), ChatTab::class.java)
+ if (chatTab.enabled) {
+ tabs.add(chatTab)
+ }
+ }
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ tabFile.delete()
+ generateNewFile()
+ }
+ }
+
+ private fun applyVersion2Changes(json: JsonObject) {
+ json.addProperty("enabled", true)
+ }
+
+ private fun applyVersion3Changes(json: JsonObject) {
+ json.add("ignore_starts", JsonArray())
+ json.add("ignore_contains", JsonArray())
+ json.add("ignore_ends", JsonArray())
+ json.add("ignore_equals", JsonArray())
+ json.add("ignore_regex", JsonArray())
+ }
+
+ private fun applyVersion4Changes(json: JsonObject) {
+ json.addProperty("color", TabButton.color)
+ json.addProperty("hovered_color", TabButton.hoveredColor)
+ json.addProperty("selected_color", TabButton.selectedColor)
+ }
+
+ private fun applyVersion5Changes(json: JsonObject) {
+ json.addProperty("lowercase", false)
+ }
+
+ private fun applyVersion6Changes(json: JsonObject) {
+ if (json.has("starts")) {
+ val starts = json["starts"].asJsonArray
+ var detected = false
+ starts.iterator().let {
+ while (it.hasNext()) {
+ when (it.next().asString) {
+ "To " -> {
+ detected = true
+ it.remove()
+ }
+ "From " -> {
+ detected = true
+ it.remove()
+ }
+ }
+ }
+ }
+ if (detected) {
+ json.add("regex", JsonArray().apply {
+ add(JsonPrimitive("^(?<type>§dTo|§dFrom) (?<prefix>.+): §r(?<message>.*)(?:§r)?\$"))
+ })
+ json.remove("unformatted")
+ json.addProperty("unformatted", false)
+ }
+ }
+ if (json.has("ends")) {
+ val ends = json["ends"].asJsonArray
+ var detected = false
+ ends.iterator().let {
+ while (it.hasNext()) {
+ when (it.next().asString) {
+ "§r§ehas invited you to join their party!", -> {
+ detected = true
+ it.remove()
+ }
+ }
+ }
+ }
+ if (detected) {
+ json.add("contains", JsonArray().apply {
+ add(JsonPrimitive("§r§ehas invited you to join their party!"))
+ })
+ }
+ }
+ }
+
+ fun shouldRender(message: IChatComponent): Boolean {
+ if (currentTabs.isEmpty()) return true
+ for (tab in currentTabs) {
+ if (tab?.shouldRender(message) == true) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun generateNewFile() {
+ tabFile.createNewFile()
+ val jsonObject = JsonObject()
+ val defaultTabs = generateDefaultTabs()
+ jsonObject.add("tabs", defaultTabs)
+ jsonObject.addProperty("version", ChatTabsJson.VERSION)
+ tabFile.writeText(GSON.toJson(jsonObject))
+ }
+
+ private fun generateDefaultTabs(): JsonArray {
+ val all = ChatTab(
+ true,
+ "ALL",
+ unformatted = false,
+ lowercase = false,
+ startsWith = null,
+ contains = null,
+ endsWith = null,
+ equals = null,
+ uncompiledRegex = null,
+ ignoreStartsWith = null,
+ ignoreContains = null,
+ ignoreEndsWith = null,
+ ignoreEquals = null,
+ uncompiledIgnoreRegex = null,
+ color = TabButton.color,
+ hoveredColor = TabButton.hoveredColor,
+ selectedColor = TabButton.selectedColor,
+ prefix = ""
+ )
+ val party = ChatTab(
+ true,
+ "PARTY",
+ unformatted = false,
+ lowercase = false,
+ startsWith = listOf("§r§9Party §8> ", "§r§9P §8> ", "§eThe party was transferred to §r", "§eKicked §r"),
+ contains = listOf("§r§ehas invited you to join their party!"),
+ endsWith = listOf(
+ "§r§eto the party! They have §r§c60 §r§eseconds to accept.§r",
+ "§r§ehas disbanded the party!§r",
+ "§r§ehas disconnected, they have §r§c5 §r§eminutes to rejoin before they are removed from the party.§r",
+ " §r§ejoined the party.§r",
+ " §r§ehas left the party.§r",
+ " §r§ehas been removed from the party.§r",
+ "§r§e because they were offline.§r"
+ ),
+ equals = listOf("§cThe party was disbanded because all invites expired and the party was empty§r"),
+ uncompiledRegex = listOf( //regexes from https://github.com/kwevin/Hychat-Tabs/blob/main/tabs/re-add%20prefixes%20%26%20fix%20shortened%20tags/chat.json cause i cant write regex
+ "(§r)*(§9Party §8\u003e)+(.*)",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§einvited §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§eto the party! They have §r§c60 §r§eseconds to accept\\.§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§ehas left the party\\.§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§ejoined the party\\.§r",
+ "§eYou left the party\\.§r",
+ "§eYou have joined §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)\u0027s §r§eparty!§r",
+ "§cThe party was disbanded because all invites expired and the party was empty§r",
+ "§cYou cannot invite that player since they\u0027re not online\\.§r",
+ "§eThe party leader, §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§e, warped you to §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§e\u0027s house\\.§r",
+ "§eSkyBlock Party Warp §r§7\\([0-9]+ players?\\)§r",
+ "§a. §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§f §r§awarped to your server§r",
+ "§eYou summoned §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§f §r§eto your server\\.§r",
+ "§eThe party leader, §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§e, warped you to their house\\.§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§aenabled Private Game§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§cdisabled Private Game§r",
+ "§cThe party is now muted\\. §r",
+ "§aThe party is no longer muted\\.§r",
+ "§cThere are no offline players to remove\\.§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§ehas been removed from the party\\.§r",
+ "§eThe party was transferred to §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§eby §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§e has promoted §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§eto Party Leader§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§e has promoted §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§eto Party Moderator§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§eis now a Party Moderator§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r§e has demoted §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§eto Party Member§r",
+ "§cYou can\u0027t demote yourself!§r",
+ "§6Party Members \\([0-9]+\\)§r",
+ "§eParty Leader: §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) ?§r(?:§[a-zA-Z0-9]).§r",
+ "§eParty Members: §r(?:(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r(?:§[a-zA-Z0-9]) . §r)+",
+ "§eParty Moderators: §r(?:(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+)§r(?:§[a-zA-Z0-9]) . §r)+",
+ "§eThe party invite to §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§ehas expired§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§cdisabled All Invite§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§aenabled All Invite§r",
+ "§cYou cannot invite that player\\.§r",
+ "§cYou are not allowed to invite players\\.§r",
+ "§eThe party leader, §r(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§ehas disconnected, they have §r§c5 §r§eminutes to rejoin before the party is disbanded\\.§r",
+ "(?:(?:§[a-zA-Z0-9])*\\[(?:(?:VIP)|(?:VIP§r§6\\+)|(?:MVP)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+)|(?:MVP(?:§r)?(?:§[a-zA-Z0-9])\\+\\+)|(?:(?:§r)?§fYOUTUBE))(?:§r)?(?:(?:§[a-zA-Z0-9]))?\\] [a-zA-Z0-9_]+|§7[a-zA-Z0-9_]+) §r§ehas disconnected, they have §r§c5 §r§eminutes to rejoin before they are removed from the party.§r",
+ "§cYou are not in a party right now\\.§r",
+ "§cThis party is currently muted\\.§r",
+ "(§r)*(§9P §8\u003e)+(.*)"
+ ),
+ ignoreStartsWith = null,
+ ignoreContains = null,
+ ignoreEndsWith = null,
+ ignoreEquals = null,
+ uncompiledIgnoreRegex = null,
+ color = TabButton.color,
+ hoveredColor = TabButton.hoveredColor,
+ selectedColor = TabButton.selectedColor,
+ prefix = "/pc "
+ )
+ val guild = ChatTab(
+ true,
+ "GUILD",
+ unformatted = true,
+ lowercase = false,
+ startsWith = listOf("Guild >", "G >"),
+ contains = null,
+ endsWith = null,
+ equals = null,
+ uncompiledRegex = null,
+ ignoreStartsWith = null,
+ ignoreContains = null,
+ ignoreEndsWith = null,
+ ignoreEquals = null,
+ uncompiledIgnoreRegex = null,
+ color = TabButton.color,
+ hoveredColor = TabButton.hoveredColor,
+ selectedColor = TabButton.selectedColor,
+ prefix = "/gc "
+ )
+ val pm = ChatTab(
+ true,
+ "PM",
+ unformatted = false,
+ lowercase = false,
+ startsWith = null,
+ contains = null,
+ endsWith = null,
+ equals = null,
+ uncompiledRegex = listOf("^(?<type>§dTo|§dFrom) (?<prefix>.+): §r(?<message>.*)(?:§r)?\$"),
+ ignoreStartsWith = null,
+ ignoreContains = null,
+ ignoreEndsWith = null,
+ ignoreEquals = null,
+ uncompiledIgnoreRegex = null,
+ color = TabButton.color,
+ hoveredColor = TabButton.hoveredColor,
+ selectedColor = TabButton.selectedColor,
+ prefix = "/r "
+ )
+ tabs.add(all)
+ tabs.add(party)
+ tabs.add(guild)
+ tabs.add(pm)
+ val jsonArray = JsonArray()
+ jsonArray.add(PARSER.parse(GSON.toJson(all)).asJsonObject)
+ jsonArray.add(PARSER.parse(GSON.toJson(party)).asJsonObject)
+ jsonArray.add(PARSER.parse(GSON.toJson(guild)).asJsonObject)
+ jsonArray.add(PARSER.parse(GSON.toJson(pm)).asJsonObject)
+ return jsonArray
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/chat/ChatTabsJson.kt b/src/main/kotlin/org/polyfrost/chatting/chat/ChatTabsJson.kt
new file mode 100644
index 0000000..c5939c3
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/chat/ChatTabsJson.kt
@@ -0,0 +1,15 @@
+package org.polyfrost.chatting.chat
+
+import com.google.gson.JsonArray
+import com.google.gson.annotations.SerializedName
+
+data class ChatTabsJson(@SerializedName("tabs") val tabs: JsonArray, var version: Int) {
+
+ override fun toString(): String {
+ return "{\"tabs\": $tabs, \"version\": $version}"
+ }
+
+ companion object {
+ const val VERSION = 6
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/command/ChattingCommand.kt b/src/main/kotlin/org/polyfrost/chatting/command/ChattingCommand.kt
new file mode 100644
index 0000000..86aff72
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/command/ChattingCommand.kt
@@ -0,0 +1,14 @@
+package org.polyfrost.chatting.command
+
+import cc.polyfrost.oneconfig.utils.commands.annotations.Command
+import cc.polyfrost.oneconfig.utils.commands.annotations.Main
+import org.polyfrost.chatting.Chatting
+import org.polyfrost.chatting.config.ChattingConfig
+
+@Command(value = Chatting.ID, description = "Access the " + Chatting.NAME + " GUI.")
+class ChattingCommand {
+ @Main
+ fun main() {
+ ChattingConfig.openGui()
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/config/ChattingConfig.kt b/src/main/kotlin/org/polyfrost/chatting/config/ChattingConfig.kt
new file mode 100644
index 0000000..0701471
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/config/ChattingConfig.kt
@@ -0,0 +1,313 @@
+package org.polyfrost.chatting.config
+
+import cc.polyfrost.oneconfig.config.Config
+import cc.polyfrost.oneconfig.config.annotations.*
+import cc.polyfrost.oneconfig.config.core.OneColor
+import cc.polyfrost.oneconfig.config.data.InfoType
+import cc.polyfrost.oneconfig.config.data.Mod
+import cc.polyfrost.oneconfig.config.data.ModType
+import cc.polyfrost.oneconfig.config.migration.VigilanceMigrator
+import cc.polyfrost.oneconfig.utils.hypixel.HypixelUtils
+import org.polyfrost.chatting.Chatting
+import org.polyfrost.chatting.chat.ChatShortcuts
+import org.polyfrost.chatting.chat.ChatTab
+import org.polyfrost.chatting.chat.ChatTabs
+import org.polyfrost.chatting.gui.components.TabButton
+import org.polyfrost.chatting.hook.ChatLineHook
+import org.polyfrost.chatting.utils.ModCompatHooks
+import java.io.File
+
+object ChattingConfig : Config(
+ Mod(
+ Chatting.NAME,
+ ModType.UTIL_QOL,
+ "/chatting_dark.svg",
+ VigilanceMigrator(File(Chatting.oldModDir, Chatting.ID + ".toml").toPath().toString())
+ ), "chatting.json"
+) {
+
+ @Dropdown(
+ name = "Text Render Type", category = "General", options = ["No Shadow", "Shadow", "Full Shadow"],
+ description = "Specifies how text should be rendered in the chat. Full Shadow displays a shadow on all sides of the text, while Shadow only displays a shadow on the right and bottom sides of the text."
+ )
+ var textRenderType = 1
+
+ @Color(
+ name = "Chat Background Color", category = "General",
+ description = "The color of the chat background."
+ )
+ var chatBackgroundColor = OneColor(0, 0, 0, 128)
+
+ @Color(
+ name = "Copy Chat Message Background Color", category = "General",
+ description = "The color of the chat background when hovering over a message."
+ )
+ var hoveredChatBackgroundColor = OneColor(80, 80, 80, 128)
+
+ @Switch(
+ name = "Right Click to Copy Chat Message", category = "General",
+ description = "Enable right clicking on a chat message to copy it."
+ )
+ var rightClickCopy = false
+
+ @Switch(
+ name = "Compact Input Box", category = "General",
+ description = "Make the chat input box the same width as the chat box."
+ )
+ var compactInputBox = false
+
+ @Color(
+ name = "Input Box Background Color", category = "General",
+ description = "The color of the chat input box background."
+ )
+ var inputBoxBackgroundColor = OneColor(0, 0, 0, 128)
+
+ @Color(
+ name = "Chat Button Background Color", category = "General",
+ description = "The color of the chat button background."
+ )
+ var chatButtonBackgroundColor = OneColor(0, 0, 0, 128)
+
+ @Color(
+ name = "Chat Button Hovered Background Color", category = "General",
+ description = "The color of the chat button background when hovered."
+ )
+ var chatButtonHoveredBackgroundColor = OneColor(255, 255, 255, 128)
+
+ @Switch(
+ name = "Inform Outdated Mods", category = "General",
+ description = "Inform the user when a mod can be replaced by Chatting."
+ )
+ var informForAlternatives = true
+
+ @Switch(
+ name = "Smooth Chat Messages",
+ category = "Animations", subcategory = "Messages",
+ description = "Smoothly animate chat messages when they appear."
+ )
+ var smoothChat = true
+
+ @Slider(
+ name = "Message Animation Speed",
+ category = "Animations", subcategory = "Messages",
+ min = 0.0f, max = 1.0f,
+ description = "The speed at which chat messages animate."
+ )
+ var messageSpeed = 0.5f
+
+ @Switch(
+ name = "Smooth Chat Scrolling",
+ category = "Animations", subcategory = "Scrolling",
+ description = "Smoothly animate scrolling when scrolling through the chat."
+ )
+ var smoothScrolling = true
+
+ @Slider(
+ name = "Scrolling Animation Speed",
+ category = "Animations", subcategory = "Scrolling",
+ min = 0.0f, max = 1.0f,
+ description = "The speed at which scrolling animates."
+ )
+ var scrollingSpeed = 0.15f
+
+ @Switch(
+ name = "Remove Scroll Bar",
+ category = "Animations", subcategory = "Scrolling",
+ description = "Removes the vanilla scroll bar from the chat."
+ )
+ var removeScrollBar = true
+
+ @Switch(
+ name = "Show Chat Heads", description = "Show the chat heads of players in chat", category = "Chat Heads",
+ )
+ var showChatHeads = true
+
+ @Switch(
+ name = "Offset Non-Player Messages",
+ description = "Offset all messages, even if a player has not been detected.",
+ category = "Chat Heads"
+ )
+ var offsetNonPlayerMessages = false
+
+ @Switch(
+ name = "Hide Chat Head on Consecutive Messages",
+ description = "Hide the chat head if the previous message was from the same player.",
+ category = "Chat Heads"
+ )
+ var hideChatHeadOnConsecutiveMessages = true
+
+ /*/
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Show Timestamp",
+ description = "Show message timestamp.",
+ category = "General"
+ )
+ var showTimestamp = false
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Timestamp Only On Hover",
+ description = "Show timestamp only on mouse hover.",
+ category = "General"
+ )
+ var showTimestampHover = true
+
+ */
+
+ @Info(
+ text = "If Chatting detects a public chat message that seems like spam, and the probability is higher than this, it will hide it.\n" + "Made for Hypixel Skyblock. Set to 100% to disable. 95% is a reasonable threshold to use it at.\n" + "Note that this is not and never will be 100% accurate; however, it's pretty much guaranteed to block most spam.",
+ size = 2,
+ category = "Player Chats",
+ type = InfoType.INFO
+ )
+ var ignored = false
+
+ @Slider(
+ min = 80F, max = 100F, name = "Spam Blocker Threshold", category = "Player Chats"
+ )
+ var spamThreshold = 100
+
+ @Switch(
+ name = "Custom SkyBlock Chat Formatting (remove ranks)", category = "Player Chats"
+ )
+ var customChatFormatting = false
+
+ @Switch(
+ name = "Completely Hide Spam", category = "Player Chats"
+ )
+ var hideSpam = false
+
+ @Switch(
+ name = "Custom Chat Height", category = "Chat Window",
+ description = "Set a custom height for the chat window. Allows for more customization than the vanilla chat height options."
+ )
+ var customChatHeight = false
+
+ @Slider(
+ min = 180F, max = 2160F, name = "Focused Height (px)", category = "Chat Window",
+ description = "The height of the chat window when focused."
+ )
+ var focusedHeight = 180
+
+ @Slider(
+ min = 180F, max = 2160F, name = "Unfocused Height (px)", category = "Chat Window",
+ description = "The height of the chat window when unfocused."
+ )
+ var unfocusedHeight = 180
+
+ @Dropdown(
+ name = "Screenshot Mode", category = "Screenshotting", options = ["Save To System", "Add To Clipboard", "Both"],
+ description = "What to do when taking a screenshot."
+ )
+ var copyMode = 0
+
+ @Checkbox(
+ name = "Chat Searching", category = "Searching",
+ description = "Enable searching through chat messages."
+ )
+ var chatSearch = true
+
+ @Switch(
+ name = "Chat Tabs", category = "Tabs",
+ description = "Allow filtering chat messages by a tab."
+ )
+ var chatTabs = true
+ get() {
+ if (!field) return false
+ return if (hypixelOnlyChatTabs) {
+ HypixelUtils.INSTANCE.isHypixel
+ } else {
+ true
+ }
+ }
+
+ @Checkbox(
+ name = "Enable Tabs Only on Hypixel", category = "Tabs",
+ description = "Only enable chat tabs on Hypixel"
+ )
+ var hypixelOnlyChatTabs = true
+
+ @Switch(
+ name = "Chat Shortcuts", category = "Shortcuts"
+ )
+ var chatShortcuts = false
+ get() {
+ if (!field) return false
+ return if (hypixelOnlyChatShortcuts) {
+ HypixelUtils.INSTANCE.isHypixel
+ } else {
+ true
+ }
+ }
+
+ @Checkbox(
+ name = "Enable Shortcuts Only on Hypixel", category = "Shortcuts"
+ )
+ var hypixelOnlyChatShortcuts = true
+
+ @Switch(
+ name = "Remove Tooltip Background", category = "Tooltips",
+ description = "Removes the background from tooltips."
+ )
+ var removeTooltipBackground = false
+
+ @Dropdown(
+ name = "Tooltip Text Render Type", category = "Tooltips", options = ["No Shadow", "Shadow", "Full Shadow"],
+ description = "The type of shadow to render on tooltips."
+ )
+ var tooltipTextRenderType = 1
+
+ init {
+ initialize()
+ addDependency("offsetNonPlayerMessages", "showChatHeads")
+ addDependency("hideChatHeadOnConsecutiveMessages", "showChatHeads")
+ addDependency("hypixelOnlyChatTabs", "chatTabs")
+ addDependency("hypixelOnlyChatShortcuts", "chatShortcuts")
+ addDependency("focusedHeight", "customChatHeight")
+ addDependency("unfocusedHeight", "customChatHeight")
+ addDependency("scrollingSpeed", "smoothScrolling")
+ addDependency("messageSpeed", "smoothChat")
+ addDependency("smoothChat", "BetterChat Smooth Chat") {
+ return@addDependency !ModCompatHooks.betterChatSmoothMessages
+ }
+ addListener("hideChatHeadOnConsecutiveMessages") {
+ ChatLineHook.chatLines.map { it.get() as ChatLineHook? }.forEach { it?.updatePlayerInfo() }
+ }
+ addListener("chatTabs") {
+ ChatTabs.initialize()
+ if (!chatTabs) {
+ val dummy = ChatTab(
+ true,
+ "ALL",
+ unformatted = false,
+ lowercase = false,
+ startsWith = null,
+ contains = null,
+ endsWith = null,
+ equals = null,
+ uncompiledRegex = null,
+ ignoreStartsWith = null,
+ ignoreContains = null,
+ ignoreEndsWith = null,
+ ignoreEquals = null,
+ uncompiledIgnoreRegex = null,
+ color = TabButton.color,
+ hoveredColor = TabButton.hoveredColor,
+ selectedColor = TabButton.selectedColor,
+ prefix = ""
+ )
+ dummy.initialize()
+ ChatTabs.currentTabs.clear()
+ ChatTabs.currentTabs.add(dummy)
+ } else {
+ ChatTabs.currentTabs.clear()
+ ChatTabs.currentTabs.add(ChatTabs.tabs[0])
+ }
+ }
+ addListener("chatShortcuts") {
+ ChatShortcuts.initialize()
+ }
+ // addDependency("showTimestampHover", "showTimestamp")
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/gui/components/CleanButton.kt b/src/main/kotlin/org/polyfrost/chatting/gui/components/CleanButton.kt
new file mode 100644
index 0000000..d4c4acd
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/gui/components/CleanButton.kt
@@ -0,0 +1,103 @@
+package org.polyfrost.chatting.gui.components
+
+import cc.polyfrost.oneconfig.renderer.TextRenderer
+import org.polyfrost.chatting.Chatting
+import org.polyfrost.chatting.config.ChattingConfig
+import club.sk1er.patcher.config.PatcherConfig
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiButton
+import net.minecraft.client.renderer.GlStateManager
+import org.polyfrost.chatting.hook.GuiNewChatHook
+
+/**
+ * Taken from ChatShortcuts under MIT License
+ * https://github.com/P0keDev/ChatShortcuts/blob/master/LICENSE
+ * @author P0keDev
+ */
+open class CleanButton(
+ buttonId: Int,
+ private val x: () -> Int,
+ private val y: () -> Int,
+ widthIn: Int,
+ heightIn: Int,
+ name: String,
+ private val renderType: () -> RenderType,
+ private val textColor: (packedFGColour: Int, enabled: Boolean, hovered: Boolean) -> Int = { packedFGColour: Int, enabled: Boolean, hovered: Boolean ->
+ var j = 14737632
+ if (packedFGColour != 0) {
+ j = packedFGColour
+ } else if (!enabled) {
+ j = 10526880
+ } else if (hovered) {
+ j = 16777120
+ }
+ j
+ },
+) :
+ GuiButton(buttonId, x.invoke(), 0, widthIn, heightIn, name) {
+
+ open fun isEnabled(): Boolean {
+ return false
+ }
+
+ open fun onMousePress() {
+
+ }
+
+ override fun mousePressed(mc: Minecraft, mouseX: Int, mouseY: Int): Boolean {
+ val isPressed =
+ visible && mouseX >= xPosition && mouseY >= yPosition && mouseX < xPosition + width && mouseY < yPosition + height
+ if (isPressed) {
+ onMousePress()
+ }
+ return isPressed
+ }
+
+ override fun drawButton(mc: Minecraft, mouseX: Int, mouseY: Int) {
+ enabled = isEnabled()
+ xPosition = x()
+ yPosition = y()
+ if (visible) {
+ val fontrenderer = mc.fontRendererObj
+ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f)
+ hovered =
+ mouseX >= xPosition && mouseY >= yPosition && mouseX < xPosition + width && mouseY < yPosition + height
+ if (!Chatting.isPatcher || !PatcherConfig.transparentChatInputField) {
+ drawRect(
+ xPosition,
+ yPosition,
+ xPosition + width,
+ yPosition + height,
+ getBackgroundColor(hovered)
+ )
+ }
+ mouseDragged(mc, mouseX, mouseY)
+ val j = textColor(packedFGColour, enabled, hovered)
+ when (renderType()) {
+ RenderType.NONE, RenderType.SHADOW -> {
+ drawCenteredString(
+ fontrenderer,
+ displayString,
+ xPosition + width / 2,
+ yPosition + (height - 8) / 2,
+ j
+ )
+ }
+
+ RenderType.FULL -> {
+ TextRenderer.drawBorderedText(
+ displayString,
+ ((xPosition + width / 2) - (fontrenderer.getStringWidth(displayString) / 2)).toFloat(),
+ (yPosition + (height - 8) / 2).toFloat(),
+ j,
+ (Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatHook).textOpacity
+ )
+ }
+ }
+ }
+ }
+
+ private fun getBackgroundColor(hovered: Boolean) =
+ if (hovered) ChattingConfig.chatButtonHoveredBackgroundColor.rgb
+ else ChattingConfig.chatButtonBackgroundColor.rgb
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/gui/components/ClearButton.kt b/src/main/kotlin/org/polyfrost/chatting/gui/components/ClearButton.kt
new file mode 100644
index 0000000..535cfca
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/gui/components/ClearButton.kt
@@ -0,0 +1,42 @@
+package org.polyfrost.chatting.gui.components
+
+import cc.polyfrost.oneconfig.libs.universal.ChatColor
+import cc.polyfrost.oneconfig.libs.universal.UChat
+import cc.polyfrost.oneconfig.libs.universal.UResolution
+import cc.polyfrost.oneconfig.utils.Multithreading
+import org.polyfrost.chatting.Chatting
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.Gui
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.util.ResourceLocation
+
+class ClearButton :
+ CleanButton(13379014, { UResolution.scaledWidth - 28 }, { UResolution.scaledHeight - 27 }, 12, 12, "",
+ { RenderType.NONE }) {
+
+ var times = 0
+
+ override fun onMousePress() {
+ ++times
+ if (times > 1) {
+ times = 0
+ Minecraft.getMinecraft().ingameGUI.chatGUI.clearChatMessages()
+ } else {
+ UChat.chat(ChatColor.RED + ChatColor.BOLD.toString() + "Click again to clear the chat!")
+ Multithreading.runAsync {
+ Thread.sleep(3000)
+ times = 0
+ }
+ }
+ }
+
+ override fun drawButton(mc: Minecraft, mouseX: Int, mouseY: Int) {
+ super.drawButton(mc, mouseX, mouseY)
+ if (visible) {
+ if (hovered) GlStateManager.color(1f, 1f, 160f / 255f)
+ else GlStateManager.color(1f, 1f, 1f)
+ mc.textureManager.bindTexture(ResourceLocation(Chatting.ID, "delete.png"))
+ Gui.drawModalRectWithCustomSizedTexture(xPosition + 1, yPosition + 1, 0f, 0f, 10, 10, 10f, 10f)
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/gui/components/RenderType.kt b/src/main/kotlin/org/polyfrost/chatting/gui/components/RenderType.kt
new file mode 100644
index 0000000..a150d64
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/gui/components/RenderType.kt
@@ -0,0 +1,7 @@
+package org.polyfrost.chatting.gui.components
+
+enum class RenderType {
+ NONE,
+ SHADOW,
+ FULL
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/gui/components/ScreenshotButton.kt b/src/main/kotlin/org/polyfrost/chatting/gui/components/ScreenshotButton.kt
new file mode 100644
index 0000000..d8da4ad
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/gui/components/ScreenshotButton.kt
@@ -0,0 +1,36 @@
+package org.polyfrost.chatting.gui.components
+
+import cc.polyfrost.oneconfig.libs.universal.UResolution
+import cc.polyfrost.oneconfig.libs.universal.UScreen
+import org.polyfrost.chatting.Chatting
+import org.polyfrost.chatting.mixin.GuiNewChatAccessor
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.Gui
+import net.minecraft.client.gui.GuiChat
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.util.ResourceLocation
+
+class ScreenshotButton :
+ CleanButton(448318, { UResolution.scaledWidth - 42 }, { UResolution.scaledHeight - 27 }, 12, 12, "",
+ { RenderType.NONE }) {
+
+ override fun onMousePress() {
+ val chat = Minecraft.getMinecraft().ingameGUI.chatGUI
+ if (UScreen.currentScreen is GuiChat) {
+ Chatting.screenshotChat((chat as GuiNewChatAccessor).scrollPos)
+ }
+ }
+
+ override fun drawButton(mc: Minecraft, mouseX: Int, mouseY: Int) {
+ super.drawButton(mc, mouseX, mouseY)
+ if (visible) {
+ if (hovered) {
+ GlStateManager.color(1f, 1f, 160f / 255f)
+ } else {
+ GlStateManager.color(1f, 1f, 1f)
+ }
+ mc.textureManager.bindTexture(ResourceLocation(Chatting.ID, "screenshot.png"))
+ Gui.drawModalRectWithCustomSizedTexture(xPosition + 1, yPosition + 1, 0f, 0f, 10, 10, 10f, 10f)
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/gui/components/SearchButton.kt b/src/main/kotlin/org/polyfrost/chatting/gui/components/SearchButton.kt
new file mode 100644
index 0000000..7981945
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/gui/components/SearchButton.kt
@@ -0,0 +1,70 @@
+package org.polyfrost.chatting.gui.components
+
+import cc.polyfrost.oneconfig.libs.universal.UResolution
+import org.polyfrost.chatting.Chatting
+import org.polyfrost.chatting.chat.ChatSearchingManager
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.Gui
+import net.minecraft.client.gui.GuiTextField
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.util.ResourceLocation
+
+class SearchButton :
+ CleanButton(3993935, { UResolution.scaledWidth - 14 }, { UResolution.scaledHeight - 27 }, 12, 12, "",
+ { RenderType.NONE }) {
+ val inputField = SearchTextField()
+ private var chatBox = false
+
+ override fun isEnabled(): Boolean {
+ return chatBox
+ }
+
+ override fun onMousePress() {
+ chatBox = !chatBox
+ inputField.setEnabled(chatBox)
+ inputField.isFocused = chatBox
+ ChatSearchingManager.lastSearch = ""
+ inputField.text = ""
+ }
+
+ override fun drawButton(mc: Minecraft, mouseX: Int, mouseY: Int) {
+ inputField.drawTextBox()
+ super.drawButton(mc, mouseX, mouseY)
+ if (visible) {
+ mc.textureManager.bindTexture(ResourceLocation(Chatting.ID, "search.png"))
+ if (isEnabled()) {
+ GlStateManager.color(224f / 255f, 224f / 255f, 224f / 255f)
+ } else if (mouseX >= xPosition && mouseX <= xPosition + 10 && mouseY >= yPosition && mouseY <= yPosition + 10) {
+ GlStateManager.color(1f, 1f, 160f / 255f)
+ } else {
+ GlStateManager.color(1f, 1f, 1f)
+ }
+ Gui.drawModalRectWithCustomSizedTexture(xPosition + 1, yPosition + 1, 0f, 0f, 10, 10, 10f, 10f)
+ }
+ }
+
+ inner class SearchTextField : GuiTextField(
+ 69420,
+ Minecraft.getMinecraft().fontRendererObj,
+ UResolution.scaledWidth * 4 / 5 - 60,
+ UResolution.scaledHeight - 26,
+ UResolution.scaledWidth / 5,
+ 12
+ ) {
+
+ init {
+ maxStringLength = 100
+ enableBackgroundDrawing = true
+ isFocused = false
+ text = ""
+ setCanLoseFocus(true)
+ }
+
+ override fun drawTextBox() {
+ if (isEnabled()) {
+ if (!isFocused) isFocused = true
+ super.drawTextBox()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/gui/components/TabButton.kt b/src/main/kotlin/org/polyfrost/chatting/gui/components/TabButton.kt
new file mode 100644
index 0000000..d0743c3
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/gui/components/TabButton.kt
@@ -0,0 +1,46 @@
+package org.polyfrost.chatting.gui.components
+
+import cc.polyfrost.oneconfig.libs.universal.UKeyboard
+import cc.polyfrost.oneconfig.libs.universal.UResolution
+import org.polyfrost.chatting.chat.ChatTab
+import org.polyfrost.chatting.chat.ChatTabs
+import org.polyfrost.chatting.config.ChattingConfig
+
+class TabButton(buttonId: Int, x: Int, widthIn: Int, heightIn: Int, private val chatTab: ChatTab) :
+ CleanButton(buttonId, { x }, {
+ UResolution.scaledHeight - 26
+ }, widthIn, heightIn, chatTab.name, { RenderType.values()[ChattingConfig.textRenderType] }, { packedFGColour: Int, enabled: Boolean, hovered: Boolean ->
+ var j = chatTab.color ?: color
+ if (packedFGColour != 0) {
+ j = packedFGColour
+ } else if (!enabled) {
+ j = chatTab.selectedColor ?: selectedColor
+ } else if (hovered) {
+ j = chatTab.hoveredColor ?: hoveredColor
+ }
+ j
+ }) {
+
+ override fun onMousePress() {
+ if (UKeyboard.isShiftKeyDown()) {
+ if (ChatTabs.currentTabs.contains(chatTab)) {
+ ChatTabs.currentTabs.remove(chatTab)
+ } else {
+ ChatTabs.currentTabs.add(chatTab)
+ }
+ } else {
+ ChatTabs.currentTabs.clear()
+ ChatTabs.currentTabs.add(chatTab)
+ }
+ }
+
+ override fun isEnabled(): Boolean {
+ return ChatTabs.currentTabs.contains(chatTab)
+ }
+
+ companion object {
+ const val color: Int = 14737632
+ const val hoveredColor: Int = 16777120
+ const val selectedColor: Int = 10526880
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/utils/EaseOutQuart.kt b/src/main/kotlin/org/polyfrost/chatting/utils/EaseOutQuart.kt
new file mode 100644
index 0000000..4b6b7a5
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/utils/EaseOutQuart.kt
@@ -0,0 +1,7 @@
+package org.polyfrost.chatting.utils
+
+import cc.polyfrost.oneconfig.gui.animations.Animation
+
+class EaseOutQuart(duration: Float, start: Float, end: Float, reverse: Boolean) : Animation(duration, start, end, reverse) {
+ override fun animate(x: Float) = -1 * (x - 1) * (x - 1) * (x - 1) * (x - 1) + 1
+} \ No newline at end of file
diff --git a/src/main/kotlin/org/polyfrost/chatting/utils/ModCompatHooks.kt b/src/main/kotlin/org/polyfrost/chatting/utils/ModCompatHooks.kt
new file mode 100644
index 0000000..ad7d329
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/utils/ModCompatHooks.kt
@@ -0,0 +1,100 @@
+package org.polyfrost.chatting.utils
+
+import cc.polyfrost.oneconfig.renderer.TextRenderer
+import cc.polyfrost.oneconfig.utils.dsl.getAlpha
+import cc.polyfrost.oneconfig.utils.dsl.mc
+import org.polyfrost.chatting.Chatting.isBetterChat
+import org.polyfrost.chatting.Chatting.isPatcher
+import org.polyfrost.chatting.config.ChattingConfig.offsetNonPlayerMessages
+import org.polyfrost.chatting.config.ChattingConfig.showChatHeads
+import org.polyfrost.chatting.config.ChattingConfig.textRenderType
+import club.sk1er.patcher.config.PatcherConfig
+import com.llamalad7.betterchat.BetterChat
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.ChatLine
+import net.minecraft.client.gui.FontRenderer
+import net.minecraft.client.gui.Gui
+import net.minecraft.client.renderer.GlStateManager
+import org.polyfrost.chatting.hook.ChatLineHook
+import org.polyfrost.chatting.hook.GuiNewChatHook
+
+// This exists because mixin doesn't like dummy classes
+object ModCompatHooks {
+ @JvmStatic
+ val xOffset
+ get() = if (isBetterChat) BetterChat.getSettings().xOffset else 0
+
+ @JvmStatic
+ val yOffset
+ get() = if (isBetterChat) BetterChat.getSettings().yOffset else 0
+
+ @JvmStatic
+ val chatPosition
+ get() = if (isPatcher && PatcherConfig.chatPosition) 12 else 0
+
+ @JvmStatic
+ val betterChatSmoothMessages
+ get() = if (isBetterChat) BetterChat.getSettings().smooth else false
+
+ @JvmStatic
+ val extendedChatLength
+ get() = if (isPatcher) 32667 else 0
+
+ @JvmStatic
+ val fontRenderer: FontRenderer
+ get() = Minecraft.getMinecraft().fontRendererObj
+
+ @JvmStatic
+ fun redirectDrawString(text: String, x: Float, y: Float, color: Int, chatLine: ChatLine, screenshot: Boolean): Int {
+ var actualX = x
+ if (showChatHeads && !screenshot) {
+ val hook = chatLine as ChatLineHook
+ if (hook.hasDetected() || offsetNonPlayerMessages) {
+ actualX += 10f
+ }
+ val networkPlayerInfo = hook.playerInfo
+ if (networkPlayerInfo != null) {
+ GlStateManager.enableBlend()
+ GlStateManager.enableAlpha()
+ GlStateManager.enableTexture2D()
+ mc.textureManager.bindTexture(networkPlayerInfo.locationSkin)
+ GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0)
+ GlStateManager.color(1.0f, 1.0f, 1.0f, color.getAlpha() / 255f)
+ Gui.drawScaledCustomSizeModalRect(
+ (x).toInt(),
+ (y - 1f).toInt(),
+ 8.0f,
+ 8.0f,
+ 8,
+ 8,
+ 8,
+ 8,
+ 64.0f,
+ 64.0f
+ )
+ Gui.drawScaledCustomSizeModalRect(
+ (x).toInt(),
+ (y - 1f).toInt(),
+ 40.0f,
+ 8.0f,
+ 8,
+ 8,
+ 8,
+ 8,
+ 64.0f,
+ 64.0f
+ )
+ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f)
+ }
+ }
+ return when (textRenderType) {
+ 0 -> fontRenderer.drawString(text, actualX, y, color, false)
+ 2 -> TextRenderer.drawBorderedText(text,
+ actualX,
+ y,
+ color,
+ (Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatHook).textOpacity)
+ else -> fontRenderer.drawString(text, actualX, y, color, true)
+ }
+ }
+}
diff --git a/src/main/kotlin/org/polyfrost/chatting/utils/RenderUtils.kt b/src/main/kotlin/org/polyfrost/chatting/utils/RenderUtils.kt
new file mode 100644
index 0000000..6eaa78b
--- /dev/null
+++ b/src/main/kotlin/org/polyfrost/chatting/utils/RenderUtils.kt
@@ -0,0 +1,259 @@
+@file:JvmName("RenderUtils")
+
+package org.polyfrost.chatting.utils
+
+import cc.polyfrost.oneconfig.utils.IOUtils
+import org.polyfrost.chatting.config.ChattingConfig
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.client.renderer.texture.TextureUtil
+import net.minecraft.client.shader.Framebuffer
+import org.apache.commons.lang3.SystemUtils
+import org.lwjgl.BufferUtils
+import org.lwjgl.opengl.GL11
+import org.lwjgl.opengl.GL12
+import sun.awt.datatransfer.DataTransferer
+import sun.awt.datatransfer.SunClipboard
+import java.awt.Toolkit
+import java.awt.image.BufferedImage
+import java.io.File
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import javax.imageio.ImageIO
+
+/**
+ * Taken from https://github.com/Moulberry/HyChat
+ */
+fun createBindFramebuffer(w: Int, h: Int): Framebuffer {
+ val framebuffer = Framebuffer(w, h, false)
+ framebuffer.framebufferColor[0] = 0x36 / 255f
+ framebuffer.framebufferColor[1] = 0x39 / 255f
+ framebuffer.framebufferColor[2] = 0x3F / 255f
+ framebuffer.framebufferClear()
+ GlStateManager.matrixMode(5889)
+ GlStateManager.loadIdentity()
+ GlStateManager.ortho(0.0, w.toDouble(), h.toDouble(), 0.0, 1000.0, 3000.0)
+ GlStateManager.matrixMode(5888)
+ GlStateManager.loadIdentity()
+ GlStateManager.translate(0.0f, 0.0f, -2000.0f)
+ framebuffer.bindFramebuffer(true)
+ return framebuffer
+}
+
+/**
+ * Taken from https://github.com/Moulberry/HyChat
+ * Modified so if not on Windows just in case it will switch it to RGB and remove the transparent background.
+ */
+fun BufferedImage.copyToClipboard() {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ try {
+ val width = this.width
+ val height = this.height
+ val hdrSize = 0x28
+ val buffer: ByteBuffer = ByteBuffer.allocate(hdrSize + width * height * 4)
+ buffer.order(ByteOrder.LITTLE_ENDIAN)
+ //Header size
+ buffer.putInt(hdrSize)
+ //Width
+ buffer.putInt(width)
+ //Int32 biHeight;
+ buffer.putInt(height)
+ //Int16 biPlanes;
+ buffer.put(1.toByte())
+ buffer.put(0.toByte())
+ //Int16 biBitCount;
+ buffer.put(32.toByte())
+ buffer.put(0.toByte())
+ //Compression
+ buffer.putInt(0)
+ //Int32 biSizeImage;
+ buffer.putInt(width * height * 4)
+ buffer.putInt(0)
+ buffer.putInt(0)
+ buffer.putInt(0)
+ buffer.putInt(0)
+
+ //Image data
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val argb: Int = this.getRGB(x, height - y - 1)
+ if (argb shr 24 and 0xFF == 0) {
+ buffer.putInt(0x00000000)
+ } else {
+ buffer.putInt(argb)
+ }
+ }
+ }
+ buffer.flip()
+ val hdrSizev5 = 0x7C
+ val bufferv5: ByteBuffer = ByteBuffer.allocate(hdrSizev5 + width * height * 4)
+ bufferv5.order(ByteOrder.LITTLE_ENDIAN)
+ //Header size
+ bufferv5.putInt(hdrSizev5)
+ //Width
+ bufferv5.putInt(width)
+ //Int32 biHeight;
+ bufferv5.putInt(height)
+ //Int16 biPlanes;
+ bufferv5.put(1.toByte())
+ bufferv5.put(0.toByte())
+ //Int16 biBitCount;
+ bufferv5.put(32.toByte())
+ bufferv5.put(0.toByte())
+ //Compression
+ bufferv5.putInt(0)
+ //Int32 biSizeImage;
+ bufferv5.putInt(width * height * 4)
+ bufferv5.putInt(0)
+ bufferv5.putInt(0)
+ bufferv5.putInt(0)
+ bufferv5.putInt(0)
+ bufferv5.order(ByteOrder.BIG_ENDIAN)
+ bufferv5.putInt(-0x1000000)
+ bufferv5.putInt(0x00FF0000)
+ bufferv5.putInt(0x0000FF00)
+ bufferv5.putInt(0x000000FF)
+ bufferv5.order(ByteOrder.LITTLE_ENDIAN)
+
+ //BGRs
+ bufferv5.put(0x42.toByte())
+ bufferv5.put(0x47.toByte())
+ bufferv5.put(0x52.toByte())
+ bufferv5.put(0x73.toByte())
+ for (i in bufferv5.position() until hdrSizev5) {
+ bufferv5.put(0.toByte())
+ }
+
+ //Image data
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val argb: Int = this.getRGB(x, height - y - 1)
+ val a = argb shr 24 and 0xFF
+ var r = argb shr 16 and 0xFF
+ var g = argb shr 8 and 0xFF
+ var b = argb and 0xFF
+ r = r * a / 0xFF
+ g = g * a / 0xFF
+ b = b * a / 0xFF
+ bufferv5.putInt(a shl 24 or (r shl 16) or (g shl 8) or b)
+ }
+ }
+ bufferv5.flip()
+ val clip = Toolkit.getDefaultToolkit().systemClipboard
+ val dt = DataTransferer.getInstance()
+ val f: Field = dt.javaClass.getDeclaredField("CF_DIB")
+ f.isAccessible = true
+ val format: Long = f.getLong(null)
+ val openClipboard: Method = clip.javaClass.getDeclaredMethod("openClipboard", SunClipboard::class.java)
+ openClipboard.isAccessible = true
+ openClipboard.invoke(clip, clip)
+ val publishClipboardData: Method = clip.javaClass.getDeclaredMethod(
+ "publishClipboardData",
+ Long::class.javaPrimitiveType,
+ ByteArray::class.java
+ )
+ publishClipboardData.isAccessible = true
+ val arr: ByteArray = buffer.array()
+ publishClipboardData.invoke(clip, format, arr)
+ val closeClipboard: Method = clip.javaClass.getDeclaredMethod("closeClipboard")
+ closeClipboard.isAccessible = true
+ closeClipboard.invoke(clip)
+ return
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ val pixels: IntArray =
+ this.getRGB(0, 0, this.width, this.height, null, 0, this.width)
+ val newImage = BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_RGB)
+ newImage.setRGB(0, 0, newImage.width, newImage.height, pixels, 0, newImage.width)
+
+ try {
+ IOUtils.copyImageToClipboard(this)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+}
+
+/**
+ * Taken from https://github.com/Moulberry/HyChat
+ */
+fun Framebuffer.screenshot(file: File): BufferedImage {
+ val w = this.framebufferWidth
+ val h = this.framebufferHeight
+ val i = w * h
+ val pixelBuffer = BufferUtils.createIntBuffer(i)
+ val pixelValues = IntArray(i)
+ GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1)
+ GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1)
+ GlStateManager.bindTexture(this.framebufferTexture)
+ GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, pixelBuffer)
+ pixelBuffer[pixelValues] //Load buffer into array
+ TextureUtil.processPixelValues(pixelValues, w, h) //Flip vertically
+ val bufferedimage = BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)
+ val j = this.framebufferTextureHeight - this.framebufferHeight
+ for (k in j until this.framebufferTextureHeight) {
+ for (l in 0 until this.framebufferWidth) {
+ bufferedimage.setRGB(l, k - j, pixelValues[k * this.framebufferTextureWidth + l])
+ }
+ }
+ if (ChattingConfig.copyMode != 1) {
+ try {
+ file.parentFile.mkdirs()
+ ImageIO.write(bufferedimage, "png", file)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ return bufferedimage
+}
+/*/
+private val timePattern = Regex("\\[\\d+:\\d+:\\d+]")
+private var lastLines = mutableListOf<ChatLine>()
+fun timestampPre() {
+ if (!ChattingConfig.showTimestampHover) return
+ val drawnChatLines = (Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatAccessor).drawnChatLines
+ val chatLine = getChatLineOverMouse(UMouse.getTrueX().roundToInt(), UMouse.getTrueY().roundToInt())
+
+ lastLines.clear()
+ for (line in drawnChatLines) {
+ val chatComponent = line.chatComponent.createCopy()
+ val newline = ChatLine(line.updatedCounter, chatComponent, line.chatLineID)
+ lastLines.add(newline)
+ }
+
+ drawnChatLines.map {
+ if (it != chatLine) it.chatComponent.siblings.removeAll { itt ->
+ timePattern.find(ChatColor.stripControlCodes(itt.unformattedText)!!) != null
+ }
+ }
+}
+
+fun timestampPost() {
+ if (!ChattingConfig.showTimestampHover) return
+ val drawnChatLines = (Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatAccessor).drawnChatLines
+ drawnChatLines.clear()
+ drawnChatLines.addAll(lastLines)
+}
+
+private fun getChatLineOverMouse(mouseX: Int, mouseY: Int): ChatLine? {
+ val chat = Minecraft.getMinecraft().ingameGUI.chatGUI
+ if (!chat.chatOpen) return null
+ val scaledResolution = ScaledResolution(Minecraft.getMinecraft())
+ val i = scaledResolution.scaleFactor
+ val f = chat.chatScale
+ val j = MathHelper.floor_float((mouseX / i - 3).toFloat() / f)
+ val k = MathHelper.floor_float((mouseY / i - 27).toFloat() / f)
+ if (j < 0 || k < 0) return null
+ val drawnChatLines = (chat as GuiNewChatAccessor).drawnChatLines
+ val l = chat.lineCount.coerceAtMost(drawnChatLines.size)
+ if (j <= MathHelper.floor_float(chat.chatWidth.toFloat() / f) && k < fontRenderer.FONT_HEIGHT * l + l) {
+ val m = k / Minecraft.getMinecraft().fontRendererObj.FONT_HEIGHT + chat.scrollPos
+ if (m >= 0 && m < drawnChatLines.size)
+ return drawnChatLines[m]
+ }
+ return null
+}
+
+ */ \ No newline at end of file