aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/cc/woverflow
diff options
context:
space:
mode:
authorWyvest <45589059+Wyvest@users.noreply.github.com>2022-01-01 22:11:34 +0700
committerWyvest <45589059+Wyvest@users.noreply.github.com>2022-01-01 22:11:34 +0700
commitcc4a11c7943d9036dc71679eaa531832cd2a2f0c (patch)
treeda242580d43fa6f26f0b0b04deab82c200a5bb16 /src/main/kotlin/cc/woverflow
parent856df4d08d3c392b35f256966f6263da86fdb7ab (diff)
downloadChatting-cc4a11c7943d9036dc71679eaa531832cd2a2f0c.tar.gz
Chatting-cc4a11c7943d9036dc71679eaa531832cd2a2f0c.tar.bz2
Chatting-cc4a11c7943d9036dc71679eaa531832cd2a2f0c.zip
Chattils 1.0.0
Diffstat (limited to 'src/main/kotlin/cc/woverflow')
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/Chattils.kt160
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/chat/ChatRegexes.kt11
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/chat/ChatSearchingManager.kt47
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/chat/ChatShortcuts.kt66
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/chat/ChatTab.kt88
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/chat/ChatTabs.kt174
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/chat/ChatTabsJson.kt11
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/command/ChattilsCommand.kt15
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/config/ChattilsConfig.kt187
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutEditGui.kt90
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutViewGui.kt57
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/components/CleanButton.kt70
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/components/ScreenshotButton.kt35
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/components/SearchButton.kt69
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/components/TabButton.kt19
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/gui/components/TextBlock.kt41
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/updater/DownloadGui.kt54
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/updater/Updater.kt103
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/utils/ImageTransferable.kt22
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/utils/ListenableArrayList.kt9
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/utils/ModCompatHooks.kt46
-rw-r--r--src/main/kotlin/cc/woverflow/chattils/utils/RenderHelper.kt248
22 files changed, 1622 insertions, 0 deletions
diff --git a/src/main/kotlin/cc/woverflow/chattils/Chattils.kt b/src/main/kotlin/cc/woverflow/chattils/Chattils.kt
new file mode 100644
index 0000000..c9714a2
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/Chattils.kt
@@ -0,0 +1,160 @@
+package cc.woverflow.chattils
+
+import cc.woverflow.chattils.chat.ChatSearchingManager
+import cc.woverflow.chattils.chat.ChatShortcuts
+import cc.woverflow.chattils.chat.ChatTabs
+import cc.woverflow.chattils.command.ChattilsCommand
+import cc.woverflow.chattils.config.ChattilsConfig
+import cc.woverflow.chattils.hook.GuiNewChatHook
+import cc.woverflow.chattils.mixin.GuiNewChatAccessor
+import cc.woverflow.chattils.updater.Updater
+import cc.woverflow.chattils.utils.ModCompatHooks
+import cc.woverflow.chattils.utils.RenderHelper
+import gg.essential.api.EssentialAPI
+import gg.essential.universal.UDesktop
+import gg.essential.universal.UResolution
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.*
+import net.minecraft.client.renderer.GlStateManager
+import net.minecraft.client.settings.KeyBinding
+import net.minecraft.client.shader.Framebuffer
+import net.minecraft.util.MathHelper
+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.FMLPostInitializationEvent
+import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import org.lwjgl.input.Keyboard
+import java.awt.image.BufferedImage
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+@Mod(
+ modid = Chattils.ID,
+ name = Chattils.NAME,
+ version = Chattils.VER,
+ modLanguageAdapter = "gg.essential.api.utils.KotlinAdapter"
+)
+object Chattils {
+
+ val keybind = KeyBinding("Screenshot Chat", Keyboard.KEY_NONE, "Chattils")
+ const val NAME = "@NAME@"
+ const val VER = "@VER@"
+ const val ID = "@ID@"
+ var doTheThing = false
+ lateinit var jarFile: File
+ private set
+ var isPatcher = false
+ private set
+ var isBetterChat = false
+ private set
+
+ private val fileFormatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd_HH.mm.ss'.png'")
+
+ val modDir = File(File(Minecraft.getMinecraft().mcDataDir, "W-OVERFLOW"), NAME)
+
+ @Mod.EventHandler
+ fun onFMLPreInitialization(event: FMLPreInitializationEvent) {
+ if (!modDir.exists()) modDir.mkdirs()
+ jarFile = event.sourceFile
+ }
+
+ @Mod.EventHandler
+ fun onInitialization(event: FMLInitializationEvent) {
+ ChattilsConfig.preload()
+ ChattilsCommand.register()
+ ClientRegistry.registerKeyBinding(keybind)
+ EVENT_BUS.register(this)
+ ChatTabs.initialize()
+ ChatShortcuts.initialize()
+ Updater.update()
+ }
+
+ @Mod.EventHandler
+ fun onPostInitialization(event: FMLPostInitializationEvent) {
+ isPatcher = Loader.isModLoaded("patcher")
+ isBetterChat = Loader.isModLoaded("betterchat")
+ }
+
+ @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) ChattilsConfig.focusedHeight else ChattilsConfig.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(GuiUtilRenderComponents.splitText(line.chatComponent, i, Minecraft.getMinecraft().fontRendererObj, false, false).map { it.formattedText }.reversed(), chat.chatWidth)
+ }
+
+ private fun screenshotChat() {
+ screenshotChat(0)
+ }
+
+ fun screenshotChat(scrollPos: Int) {
+ val hud = Minecraft.getMinecraft().ingameGUI
+ val chat = hud.chatGUI
+ val chatLines = ArrayList<String>()
+ ChatSearchingManager.filterMessages((chat as GuiNewChatHook).prevText, (chat as GuiNewChatAccessor).drawnChatLines)?.let { drawnLines ->
+ val chatHeight = if (ChattilsConfig.customChatHeight) getChatHeight(true) / 9 else GuiNewChat.calculateChatboxHeight(Minecraft.getMinecraft().gameSettings.chatHeightFocused / 9)
+ for (i in scrollPos until drawnLines.size.coerceAtMost(scrollPos + chatHeight)) {
+ chatLines.add(drawnLines[i].chatComponent.formattedText)
+ }
+
+ screenshot(chatLines, chat.chatWidth)?.let {
+ RenderHelper.copyBufferedImageToClipboard(it)
+ }
+ }
+ }
+
+ private fun screenshot(messages: List<String>, width: Int): BufferedImage? {
+ if (messages.isEmpty()) {
+ EssentialAPI.getNotifications().push("Chattils", "Chat window is empty.")
+ return null
+ }
+
+ val fr: FontRenderer = ModCompatHooks.fontRenderer
+ val fb: Framebuffer = RenderHelper.createBindFramebuffer(width * 3, (messages.size * 9) * 3)
+ val file = File(Minecraft.getMinecraft().mcDataDir, "screenshots/chat/" + fileFormatter.format(Date()))
+
+ GlStateManager.scale(3f, 3f, 1f)
+ val scale = Minecraft.getMinecraft().gameSettings.chatScale
+ GlStateManager.scale(scale, scale, 1f)
+ for (i in messages.indices) {
+ fr.drawStringWithShadow(messages[i], 0f, (messages.size - 1 - i) * 9f, 0xffffff)
+ }
+
+ val image = RenderHelper.screenshotFramebuffer(fb, file)
+ Minecraft.getMinecraft().entityRenderer.setupOverlayRendering()
+ Minecraft.getMinecraft().framebuffer.bindFramebuffer(true)
+ EssentialAPI.getNotifications()
+ .push("Chattils", "Chat screenshotted successfully.\nClick to open.") {
+ if (!UDesktop.browse(file.toURI())) {
+ EssentialAPI.getNotifications().push("Chattils", "Could not browse!")
+ }
+ }
+ return image
+ }
+}
diff --git a/src/main/kotlin/cc/woverflow/chattils/chat/ChatRegexes.kt b/src/main/kotlin/cc/woverflow/chattils/chat/ChatRegexes.kt
new file mode 100644
index 0000000..0776fa0
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/chat/ChatRegexes.kt
@@ -0,0 +1,11 @@
+package cc.woverflow.chattils.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/cc/woverflow/chattils/chat/ChatSearchingManager.kt b/src/main/kotlin/cc/woverflow/chattils/chat/ChatSearchingManager.kt
new file mode 100644
index 0000000..646c218
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/chat/ChatSearchingManager.kt
@@ -0,0 +1,47 @@
+package cc.woverflow.chattils.chat
+
+import cc.woverflow.chattils.hook.GuiNewChatHook
+import gg.essential.lib.caffeine.cache.Cache
+import gg.essential.lib.caffeine.cache.Caffeine
+import gg.essential.universal.wrappers.message.UTextComponent
+import net.minecraft.client.Minecraft
+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()
+
+ @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)
+ }
+ }
+
+ @JvmStatic
+ fun setPrevText(text: String) {
+ (Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatHook).prevText = text
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/chat/ChatShortcuts.kt b/src/main/kotlin/cc/woverflow/chattils/chat/ChatShortcuts.kt
new file mode 100644
index 0000000..50cb73c
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/chat/ChatShortcuts.kt
@@ -0,0 +1,66 @@
+package cc.woverflow.chattils.chat
+
+import cc.woverflow.chattils.Chattils
+import cc.woverflow.chattils.utils.ListenableArrayList
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import java.io.File
+
+object ChatShortcuts {
+ private val shortcutsFile = File(Chattils.modDir, "chatshortcuts.json")
+ private val PARSER = JsonParser()
+
+ private var initialized = false
+
+ val shortcuts = ListenableArrayList<Pair<String, String>>({
+ it.sortWith(comparator)
+ })
+ private val comparator = Comparator<Pair<String, String>> { o1, o2 ->
+ return@Comparator o2.first.length.compareTo(o1.first.length)
+ }
+
+
+ fun initialize() {
+ if (initialized) {
+ return
+ } else {
+ initialized = true
+ }
+ if (!shortcutsFile.exists()) {
+ shortcutsFile.createNewFile()
+ shortcutsFile.writeText(
+ JsonObject().toString()
+ )
+ } else {
+ val jsonObj = PARSER.parse(shortcutsFile.readText()).asJsonObject
+ for (shortcut in jsonObj.entrySet()) {
+ shortcuts.add(shortcut.key to shortcut.value.asString)
+ }
+ }
+ }
+
+ 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/cc/woverflow/chattils/chat/ChatTab.kt b/src/main/kotlin/cc/woverflow/chattils/chat/ChatTab.kt
new file mode 100644
index 0000000..87f28a1
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/chat/ChatTab.kt
@@ -0,0 +1,88 @@
+package cc.woverflow.chattils.chat
+
+import cc.woverflow.chattils.gui.components.TabButton
+import com.google.gson.annotations.SerializedName
+import kotlinx.coroutines.runBlocking
+import net.minecraft.client.Minecraft
+import net.minecraft.util.EnumChatFormatting
+import net.minecraft.util.IChatComponent
+
+data class ChatTab(
+ val enabled: Boolean,
+ val name: String,
+ val unformatted: 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>?,
+ val prefix: String
+) {
+ lateinit var button: TabButton
+ lateinit var compiledRegex: ChatRegexes
+
+ //Ugly hack to make GSON not make button / regex null
+ fun initialize() {
+ compiledRegex = ChatRegexes(uncompiledRegex)
+ val width = Minecraft.getMinecraft().fontRendererObj.getStringWidth(name)
+ button = TabButton(653452, runBlocking {
+ val returnValue = x - 2
+ x += 6 + width
+ return@runBlocking returnValue
+ }, width + 4, 12, this)
+ }
+
+ fun shouldRender(chatComponent: IChatComponent): Boolean {
+ if (startsWith == null && equals == null && endsWith == null && contains == null && uncompiledRegex == null) {
+ return true
+ }
+ val message =
+ if (unformatted) EnumChatFormatting.getTextWithoutFormattingCodes(chatComponent.unformattedText) else chatComponent.formattedText
+ 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
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is ChatTab && name == other.name && startsWith == other.startsWith && contains == other.contains && endsWith == other.endsWith && equals == other.equals && compiledRegex == other.compiledRegex
+ }
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + (startsWith?.hashCode() ?: 0)
+ result = 31 * result + (contains?.hashCode() ?: 0)
+ result = 31 * result + (endsWith?.hashCode() ?: 0)
+ result = 31 * result + (equals?.hashCode() ?: 0)
+ result = 31 * result + (uncompiledRegex?.hashCode() ?: 0)
+ result = 31 * result + prefix.hashCode()
+ result = 31 * result + button.hashCode()
+ return result
+ }
+
+ companion object {
+ private var x = 4
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/chat/ChatTabs.kt b/src/main/kotlin/cc/woverflow/chattils/chat/ChatTabs.kt
new file mode 100644
index 0000000..ada0baa
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/chat/ChatTabs.kt
@@ -0,0 +1,174 @@
+package cc.woverflow.chattils.chat
+
+import cc.woverflow.chattils.Chattils
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+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 currentTab: ChatTab? = null
+ set(value) {
+ if (value != null) {
+ field = value
+ if (Minecraft.getMinecraft().theWorld != null) {
+ Minecraft.getMinecraft().ingameGUI.chatGUI.refreshChat()
+ }
+ }
+ }
+ private var initialized = false
+
+ private val tabFile = File(Chattils.modDir, "chattabs.json")
+
+ fun initialize() {
+ if (initialized) {
+ return
+ } else {
+ initialized = true
+ }
+ if (!tabFile.exists()) {
+ generateNewFile()
+ } else {
+ try {
+ val chatTabJson = GSON.fromJson(tabFile.readText(), ChatTabsJson::class.java)
+ if (chatTabJson.version == 1) {
+ // ver 2 adds `enabled`
+ chatTabJson.tabs.forEach {
+ it.asJsonObject.addProperty("enabled", true)
+ }
+ chatTabJson.version = 2
+ tabFile.writeText(chatTabJson.toString())
+ }
+ 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()
+ }
+ }
+ tabs.forEach {
+ it.initialize()
+ }
+ currentTab = tabs[0]
+ }
+
+ fun shouldRender(message: IChatComponent): Boolean {
+ return currentTab?.shouldRender(message) ?: true
+ }
+
+ private fun generateNewFile() {
+ tabFile.createNewFile()
+ val jsonObject = JsonObject()
+ val defaultTabs = generateDefaultTabs()
+ jsonObject.add("tabs", defaultTabs)
+ jsonObject.addProperty("version", 1)
+ tabFile.writeText(jsonObject.toString())
+ }
+
+ private fun generateDefaultTabs(): JsonArray {
+ val all = ChatTab(true, "ALL", false, null, null, null, null, null, "")
+ val party = ChatTab(
+ true,
+ "PARTY",
+ false,
+ listOf("§r§9Party §8> ", "§r§9P §8> ", "§eThe party was transferred to §r", "§eKicked §r"),
+ null,
+ listOf(
+ "§r§ehas invited you to join their party!",
+ "§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"
+ ),
+ listOf("§cThe party was disbanded because all invites expired and the party was empty§r"),
+ 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)+(.*)"
+ ),
+ "/pc "
+ )
+ val guild = ChatTab(
+ true,
+ "GUILD",
+ true,
+ listOf("Guild >", "G >"),
+ null,
+ null,
+ null,
+ null,
+ "/gc "
+ )
+ val pm = ChatTab(
+ true,
+ "PM",
+ true,
+ listOf("To ", "From "),
+ null,
+ null,
+ null,
+ null,
+ "/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/cc/woverflow/chattils/chat/ChatTabsJson.kt b/src/main/kotlin/cc/woverflow/chattils/chat/ChatTabsJson.kt
new file mode 100644
index 0000000..a63913b
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/chat/ChatTabsJson.kt
@@ -0,0 +1,11 @@
+package cc.woverflow.chattils.chat
+
+import com.google.gson.JsonArray
+import com.google.gson.annotations.SerializedName
+
+data class ChatTabsJson(@SerializedName("tabs") val tabs: JsonArray, @SerializedName("version") var version: Int) {
+
+ override fun toString(): String {
+ return "{\"tabs\": $tabs, \"version\": \"$version\"}"
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/command/ChattilsCommand.kt b/src/main/kotlin/cc/woverflow/chattils/command/ChattilsCommand.kt
new file mode 100644
index 0000000..aa162e1
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/command/ChattilsCommand.kt
@@ -0,0 +1,15 @@
+package cc.woverflow.chattils.command
+
+import cc.woverflow.chattils.Chattils
+import cc.woverflow.chattils.config.ChattilsConfig
+import gg.essential.api.EssentialAPI
+import gg.essential.api.commands.Command
+import gg.essential.api.commands.DefaultHandler
+
+object ChattilsCommand : Command(Chattils.ID, true) {
+
+ @DefaultHandler
+ fun handle() {
+ EssentialAPI.getGuiUtil().openScreen(ChattilsConfig.gui())
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/config/ChattilsConfig.kt b/src/main/kotlin/cc/woverflow/chattils/config/ChattilsConfig.kt
new file mode 100644
index 0000000..77b9654
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/config/ChattilsConfig.kt
@@ -0,0 +1,187 @@
+package cc.woverflow.chattils.config
+
+import cc.woverflow.chattils.Chattils
+import cc.woverflow.chattils.chat.ChatShortcuts
+import cc.woverflow.chattils.chat.ChatTab
+import cc.woverflow.chattils.chat.ChatTabs
+import cc.woverflow.chattils.gui.ChatShortcutViewGui
+import cc.woverflow.chattils.updater.DownloadGui
+import cc.woverflow.chattils.updater.Updater
+import gg.essential.api.EssentialAPI
+import gg.essential.vigilance.Vigilant
+import gg.essential.vigilance.data.Category
+import gg.essential.vigilance.data.Property
+import gg.essential.vigilance.data.PropertyType
+import gg.essential.vigilance.data.SortingBehavior
+import java.io.File
+
+object ChattilsConfig : Vigilant(File(Chattils.modDir, "${Chattils.ID}.toml"), Chattils.NAME, sortingBehavior = ConfigSorting) {
+
+ @Property(
+ type = PropertyType.SELECTOR,
+ name = "Text Render Type",
+ description = "Choose the type of rendering for the text.",
+ category = "General",
+ options = ["No Shadow", "Shadow", "Full Shadow"]
+ )
+ var textRenderType = 1
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Custom Chat Height",
+ description = "Allows you to change the height of chat to heights greater than before.",
+ category = "Chat Window"
+ )
+ var customChatHeight = true
+
+ @Property(
+ type = PropertyType.SLIDER,
+ min = 180,
+ max = 10000,
+ name = "Focused Height",
+ description = "Height in pixels.",
+ category = "Chat Window"
+ )
+ var focusedHeight = 180
+
+ @Property(
+ type = PropertyType.SLIDER,
+ min = 180,
+ max = 10000,
+ name = "Unfocused Height",
+ description = "Height in pixels.",
+ category = "Chat Window"
+ )
+ var unfocusedHeight = 180
+
+ @Property(
+ type = PropertyType.SELECTOR,
+ name = "Screenshot Mode",
+ description = "The mode in which screenshotting will work.",
+ category = "Screenshotting",
+ options = [
+ "Save To System",
+ "Add To Clipboard",
+ "Both"
+ ]
+ )
+ var copyMode = 0
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Chat Searching",
+ description = "Add a chat search bar.",
+ category = "Searching"
+ )
+ var chatSearch = true
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Chat Tabs",
+ description = "Add chat tabs.",
+ category = "Tabs"
+ )
+ var chatTabs = true
+ get() {
+ if (!field) return false
+ return if (hypixelOnlyChatTabs) {
+ EssentialAPI.getMinecraftUtil().isHypixel()
+ } else {
+ true
+ }
+ }
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Enable Tabs Only on Hypixel",
+ description = "Enable chat tabs only in Hypixel.",
+ category = "Tabs"
+ )
+ var hypixelOnlyChatTabs = true
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Chat Shortcuts",
+ description = "Add chat shortcuts.",
+ category = "Shortcuts"
+ )
+ var chatShortcuts = false
+ get() {
+ if (!field) return false
+ return if (hypixelOnlyChatShortcuts) {
+ EssentialAPI.getMinecraftUtil().isHypixel()
+ } else {
+ true
+ }
+ }
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Enable Shortcuts Only on Hypixel",
+ description = "Enable chat shortcuts only in Hypixel.",
+ category = "Shortcuts"
+ )
+ var hypixelOnlyChatShortcuts = true
+
+ @Property(
+ type = PropertyType.BUTTON,
+ name = "Edit Chat Shortcuts",
+ description = "Edit chat shortcuts.",
+ category = "Shortcuts"
+ )
+ fun openChatShortcutsGUI() {
+ EssentialAPI.getGuiUtil().openScreen(ChatShortcutViewGui())
+ }
+
+ @Property(
+ type = PropertyType.SWITCH,
+ name = "Show Update Notification",
+ description = "Show a notification when you start Minecraft informing you of new updates.",
+ category = "Updater"
+ )
+ var showUpdate = true
+
+ @Property(
+ type = PropertyType.BUTTON,
+ name = "Update Now",
+ description = "Update by clicking the button.",
+ category = "Updater"
+ )
+ fun update() {
+ if (Updater.shouldUpdate) EssentialAPI.getGuiUtil()
+ .openScreen(DownloadGui()) else EssentialAPI.getNotifications()
+ .push(
+ Chattils.NAME,
+ "No update had been detected at startup, and thus the update GUI has not been shown."
+ )
+ }
+
+ init {
+ initialize()
+ registerListener("chatTabs") { funny: Boolean ->
+ chatTabs = funny
+ ChatTabs.initialize()
+ if (!funny) {
+ val dummy = ChatTab(true, "ALL", false, null, null, null, null, null, "")
+ dummy.initialize()
+ ChatTabs.currentTab = dummy
+ } else {
+ ChatTabs.currentTab = ChatTabs.tabs[0]
+ }
+ }
+ registerListener("chatShortcuts") { funny: Boolean ->
+ chatShortcuts = funny
+ ChatShortcuts.initialize()
+ }
+ }
+
+ private object ConfigSorting : SortingBehavior() {
+ override fun getCategoryComparator(): Comparator<in Category> = Comparator { o1, o2 ->
+ if (o1.name == "General") return@Comparator -1
+ if (o2.name == "General") return@Comparator 1
+ else compareValuesBy(o1, o2) {
+ it.name
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutEditGui.kt b/src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutEditGui.kt
new file mode 100644
index 0000000..aa4b933
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutEditGui.kt
@@ -0,0 +1,90 @@
+package cc.woverflow.chattils.gui
+
+import cc.woverflow.chattils.chat.ChatShortcuts
+import gg.essential.api.EssentialAPI
+import gg.essential.api.gui.buildConfirmationModal
+import gg.essential.elementa.ElementaVersion
+import gg.essential.elementa.WindowScreen
+import gg.essential.elementa.components.UIBlock
+import gg.essential.elementa.constraints.CenterConstraint
+import gg.essential.elementa.constraints.SiblingConstraint
+import gg.essential.elementa.dsl.childOf
+import gg.essential.elementa.dsl.constrain
+import gg.essential.elementa.dsl.percent
+import gg.essential.elementa.dsl.pixels
+import gg.essential.vigilance.gui.VigilancePalette
+import gg.essential.vigilance.gui.settings.ButtonComponent
+import gg.essential.vigilance.gui.settings.TextComponent
+
+class ChatShortcutEditGui(private var alias: String, private var command: String, private val editing: Boolean) :
+ WindowScreen(restoreCurrentGuiOnClose = true, version = ElementaVersion.V1) {
+
+ private val initialAlias = alias
+ private val initialCommand = command
+
+ override fun initScreen(width: Int, height: Int) {
+ super.initScreen(width, height)
+ val block = UIBlock(VigilancePalette.getBackground()).constrain {
+ this.x = CenterConstraint()
+ this.y = CenterConstraint()
+ this.width = 100.pixels()
+ this.height = 100.pixels()
+ } childOf window
+ TextComponent(initialAlias, "Alias", wrap = false, protected = false).constrain {
+ x = CenterConstraint()
+ y = 10.percent()
+ }.childOf(block).onValueChange {
+ if (it is String) alias = it
+ }
+ TextComponent(initialCommand, "Command", wrap = false, protected = false).constrain {
+ x = CenterConstraint()
+ y = SiblingConstraint()
+ }.childOf(block).onValueChange {
+ if (it is String) command = it
+ }
+ if (editing) {
+ ButtonComponent("Reset") {
+ EssentialAPI.getGuiUtil().openScreen(ChatShortcutEditGui(initialAlias, initialCommand, editing))
+ } constrain {
+ x = CenterConstraint()
+ y = 70.percent()
+ } childOf window
+ }
+ ButtonComponent("Save") {
+ alias = alias.substringAfter("/")
+ command = command.substringAfter("/")
+ if (editing) {
+ ChatShortcuts.removeShortcut(initialAlias)
+ }
+ if (alias.isBlank() || command.isBlank()) {
+ return@ButtonComponent
+ }
+ if (ChatShortcuts.shortcuts.any { it.first == alias }) {
+ EssentialAPI.getGuiUtil().openScreen(ChatShortcutConfirmGui(alias, command))
+ return@ButtonComponent
+ }
+ ChatShortcuts.writeShortcut(alias, command)
+ restorePreviousScreen()
+ } constrain {
+ x = CenterConstraint()
+ y = 80.percent()
+ } childOf window
+ }
+
+ inner class ChatShortcutConfirmGui(private var alias: String, private var command: String) :
+ WindowScreen(restoreCurrentGuiOnClose = true, version = ElementaVersion.V1) {
+ override fun initScreen(width: Int, height: Int) {
+ super.initScreen(width, height)
+ EssentialAPI.getEssentialComponentFactory().buildConfirmationModal {
+ text = "An alias with this name already exists, are you sure you want to overwrite it?"
+ onConfirm = {
+ ChatShortcuts.writeShortcut(alias, command)
+ EssentialAPI.getGuiUtil().openScreen(null)
+ }
+ onDeny = {
+ restorePreviousScreen()
+ }
+ } childOf this@ChatShortcutConfirmGui.window
+ }
+ }
+}
diff --git a/src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutViewGui.kt b/src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutViewGui.kt
new file mode 100644
index 0000000..f4ad049
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/ChatShortcutViewGui.kt
@@ -0,0 +1,57 @@
+package cc.woverflow.chattils.gui
+
+import cc.woverflow.chattils.chat.ChatShortcuts
+import cc.woverflow.chattils.gui.components.TextBlock
+import gg.essential.api.EssentialAPI
+import gg.essential.elementa.ElementaVersion
+import gg.essential.elementa.WindowScreen
+import gg.essential.elementa.components.UIBlock
+import gg.essential.elementa.constraints.CenterConstraint
+import gg.essential.elementa.constraints.RelativeWindowConstraint
+import gg.essential.elementa.constraints.SiblingConstraint
+import gg.essential.elementa.dsl.*
+import gg.essential.vigilance.gui.VigilancePalette
+import gg.essential.vigilance.gui.settings.ButtonComponent
+
+class ChatShortcutViewGui : WindowScreen(version = ElementaVersion.V1) {
+ override fun initScreen(width: Int, height: Int) {
+ super.initScreen(width, height)
+ for ((index, shortcut) in ChatShortcuts.shortcuts.withIndex()) {
+ val block = UIBlock(VigilancePalette.getBackground()).constrain {
+ x = 3.percent()
+ y = (index * 12).percent()
+ this.width = 94.percent()
+ this.height = 25.pixels()
+ } childOf this.window
+ TextBlock(shortcut.first).constrain {
+ x = RelativeWindowConstraint(0.05F)
+ y = CenterConstraint()
+ } childOf block
+ TextBlock(shortcut.second).constrain {
+ x = SiblingConstraint(10F)
+ y = CenterConstraint()
+ } childOf block
+ ButtonComponent("Edit") {
+ println("${shortcut.first} ${shortcut.second}")
+ EssentialAPI.getGuiUtil().openScreen(ChatShortcutEditGui(shortcut.first, shortcut.second, true))
+ } constrain {
+ x = SiblingConstraint(20F)
+ y = CenterConstraint()
+ } childOf block
+ ButtonComponent("Delete") {
+ println("${shortcut.first} ${shortcut.second}")
+ ChatShortcuts.removeShortcut(shortcut.first)
+ EssentialAPI.getGuiUtil().openScreen(ChatShortcutViewGui())
+ } constrain {
+ x = SiblingConstraint(5F)
+ y = CenterConstraint()
+ } childOf block
+ }
+ ButtonComponent("New") {
+ EssentialAPI.getGuiUtil().openScreen(ChatShortcutEditGui("", "", false))
+ } constrain {
+ x = CenterConstraint()
+ y = 80.percent()
+ } childOf window
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/gui/components/CleanButton.kt b/src/main/kotlin/cc/woverflow/chattils/gui/components/CleanButton.kt
new file mode 100644
index 0000000..7011518
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/components/CleanButton.kt
@@ -0,0 +1,70 @@
+package cc.woverflow.chattils.gui.components
+
+import cc.woverflow.chattils.Chattils
+import club.sk1er.patcher.config.PatcherConfig
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiButton
+import net.minecraft.client.renderer.GlStateManager
+import java.awt.Color
+
+/**
+ * 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) :
+ 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.invoke()
+ yPosition = y.invoke()
+ 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 (!Chattils.isPatcher || !PatcherConfig.transparentChatInputField) {
+ drawRect(
+ xPosition,
+ yPosition,
+ xPosition + width,
+ yPosition + height,
+ if (hovered) hoveredColor else color
+ )
+ }
+ mouseDragged(mc, mouseX, mouseY)
+ var j = 14737632
+ if (packedFGColour != 0) {
+ j = packedFGColour
+ } else if (!enabled) {
+ j = 10526880
+ } else if (hovered) {
+ j = 16777120
+ }
+ drawCenteredString(fontrenderer, displayString, xPosition + width / 2, yPosition + (height - 8) / 2, j)
+ }
+ }
+
+ companion object {
+ private val hoveredColor = Color(255, 255, 255, 128).rgb
+ private val color = Color(0, 0, 0, 128).rgb
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/gui/components/ScreenshotButton.kt b/src/main/kotlin/cc/woverflow/chattils/gui/components/ScreenshotButton.kt
new file mode 100644
index 0000000..0e6f088
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/components/ScreenshotButton.kt
@@ -0,0 +1,35 @@
+package cc.woverflow.chattils.gui.components
+
+import cc.woverflow.chattils.Chattils
+import cc.woverflow.chattils.mixin.GuiNewChatAccessor
+import gg.essential.api.utils.GuiUtil
+import gg.essential.universal.UResolution
+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 - 28 }, { UResolution.scaledHeight - 27 }, 12, 12, "") {
+
+ override fun onMousePress() {
+ val chat = Minecraft.getMinecraft().ingameGUI.chatGUI
+ if (GuiUtil.getOpenedScreen() is GuiChat) {
+ Chattils.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(Chattils.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/cc/woverflow/chattils/gui/components/SearchButton.kt b/src/main/kotlin/cc/woverflow/chattils/gui/components/SearchButton.kt
new file mode 100644
index 0000000..04a2743
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/components/SearchButton.kt
@@ -0,0 +1,69 @@
+package cc.woverflow.chattils.gui.components
+
+import cc.woverflow.chattils.Chattils
+import cc.woverflow.chattils.hook.GuiNewChatHook
+import gg.essential.universal.UResolution
+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, "") {
+ val inputField = SearchTextField()
+ private var chatBox = false
+
+ override fun isEnabled(): Boolean {
+ return chatBox
+ }
+
+ override fun onMousePress() {
+ chatBox = !chatBox
+ inputField.setEnabled(chatBox)
+ inputField.isFocused = chatBox
+ (Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatHook).prevText = ""
+ inputField.text = ""
+ }
+
+ override fun drawButton(mc: Minecraft, mouseX: Int, mouseY: Int) {
+ inputField.drawTextBox()
+ super.drawButton(mc, mouseX, mouseY)
+ if (visible) {
+ mc.textureManager.bindTexture(ResourceLocation(Chattils.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 - 27,
+ 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()
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/gui/components/TabButton.kt b/src/main/kotlin/cc/woverflow/chattils/gui/components/TabButton.kt
new file mode 100644
index 0000000..4770900
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/components/TabButton.kt
@@ -0,0 +1,19 @@
+package cc.woverflow.chattils.gui.components
+
+import cc.woverflow.chattils.chat.ChatTab
+import cc.woverflow.chattils.chat.ChatTabs
+import gg.essential.universal.UResolution
+
+class TabButton(buttonId: Int, x: Int, widthIn: Int, heightIn: Int, private val chatTab: ChatTab) :
+ CleanButton(buttonId, { x }, {
+ UResolution.scaledHeight - 26
+ }, widthIn, heightIn, chatTab.name) {
+
+ override fun onMousePress() {
+ ChatTabs.currentTab = chatTab
+ }
+
+ override fun isEnabled(): Boolean {
+ return chatTab != ChatTabs.currentTab
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/gui/components/TextBlock.kt b/src/main/kotlin/cc/woverflow/chattils/gui/components/TextBlock.kt
new file mode 100644
index 0000000..c83d22d
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/gui/components/TextBlock.kt
@@ -0,0 +1,41 @@
+package cc.woverflow.chattils.gui.components
+
+import gg.essential.elementa.components.UIBlock
+import gg.essential.elementa.components.UIText
+import gg.essential.elementa.constraints.ChildBasedSizeConstraint
+import gg.essential.elementa.dsl.*
+import gg.essential.elementa.effects.OutlineEffect
+import gg.essential.elementa.state.BasicState
+import gg.essential.vigilance.gui.VigilancePalette
+import gg.essential.vigilance.gui.settings.SettingComponent
+
+/**
+ * Heavily modified from Vigilance under LGPLv3 (modified to be just a text block)
+ * https://github.com/Sk1erLLC/Vigilance/blob/master/LICENSE
+ */
+class TextBlock(
+ text: String
+) : SettingComponent() {
+ private val textHolder = UIBlock() constrain {
+ width = ChildBasedSizeConstraint() + 6.pixels()
+ height = ChildBasedSizeConstraint() + 6.pixels()
+ color = VigilancePalette.getDarkHighlight().toConstraint()
+ } childOf this effect OutlineEffect(
+ VigilancePalette.getDivider(),
+ 1f
+ ).bindColor(BasicState(VigilancePalette.getDivider()))
+
+ private val text: UIText = UIText(text) constrain {
+ x = 3.pixels()
+ y = 3.pixels()
+ }
+
+ init {
+ this.text childOf textHolder
+
+ constrain {
+ width = ChildBasedSizeConstraint()
+ height = ChildBasedSizeConstraint()
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/updater/DownloadGui.kt b/src/main/kotlin/cc/woverflow/chattils/updater/DownloadGui.kt
new file mode 100644
index 0000000..c7f59ec
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/updater/DownloadGui.kt
@@ -0,0 +1,54 @@
+package cc.woverflow.chattils.updater
+
+import cc.woverflow.chattils.Chattils
+import gg.essential.api.EssentialAPI
+import gg.essential.api.gui.buildConfirmationModal
+import gg.essential.api.utils.Multithreading
+import gg.essential.elementa.ElementaVersion
+import gg.essential.elementa.WindowScreen
+import gg.essential.elementa.dsl.childOf
+import java.io.File
+
+class DownloadGui : WindowScreen(ElementaVersion.V1, true, true, true, -1) {
+ override fun initScreen(width: Int, height: Int) {
+ super.initScreen(width, height)
+ EssentialAPI.getEssentialComponentFactory().buildConfirmationModal {
+ this.text = "Are you sure you want to update?"
+ this.secondaryText =
+ "(This will update from v${Chattils.VER} to ${Updater.latestTag})"
+ this.onConfirm = {
+ restorePreviousScreen()
+ Multithreading.runAsync {
+ if (Updater.download(
+ Updater.updateUrl,
+ File(
+ "mods/${Chattils.NAME}-${
+ Updater.latestTag!!.substringAfter("v")
+ }.jar"
+ )
+ ) && Updater.download(
+ "https://github.com/Wyvest/Deleter/releases/download/v1.2/Deleter-1.2.jar",
+ File(Chattils.modDir.parentFile, "Deleter-1.2.jar")
+ )
+ ) {
+ EssentialAPI.getNotifications()
+ .push(
+ Chattils.NAME,
+ "The ingame updater has successfully installed the newest version."
+ )
+ Updater.addShutdownHook()
+ Updater.shouldUpdate = false
+ } else {
+ EssentialAPI.getNotifications().push(
+ Chattils.NAME,
+ "The ingame updater has NOT installed the newest version as something went wrong."
+ )
+ }
+ }
+ }
+ this.onDeny = {
+ restorePreviousScreen()
+ }
+ } childOf this.window
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/updater/Updater.kt b/src/main/kotlin/cc/woverflow/chattils/updater/Updater.kt
new file mode 100644
index 0000000..35b6e79
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/updater/Updater.kt
@@ -0,0 +1,103 @@
+package cc.woverflow.chattils.updater
+
+import cc.woverflow.chattils.Chattils
+import cc.woverflow.chattils.config.ChattilsConfig
+import gg.essential.api.EssentialAPI
+import gg.essential.api.utils.Multithreading
+import gg.essential.api.utils.WebUtil.downloadToFile
+import gg.essential.api.utils.WebUtil.fetchJSON
+import gg.essential.universal.UDesktop.open
+import net.minecraft.client.Minecraft
+import net.minecraftforge.fml.common.versioning.DefaultArtifactVersion
+import java.io.File
+import java.io.IOException
+
+object Updater {
+ var updateUrl = ""
+ var latestTag: String? = null
+ var shouldUpdate = false
+
+ fun update() {
+ Multithreading.runAsync {
+ try {
+ val latestRelease =
+ fetchJSON("https://api.github.com/repos/W-OVERFLOW/${Chattils.ID}/releases/latest").getObject()
+ latestTag = latestRelease["tag_name"].asString
+ val currentVersion =
+ DefaultArtifactVersion(Chattils.VER.substringBefore("-"))
+ val latestVersion = DefaultArtifactVersion(latestTag!!.substringAfter("v").substringBefore("-"))
+ if (currentVersion >= latestVersion) {
+ if (currentVersion != latestVersion || !Chattils.VER.contains("-")) {
+ return@runAsync
+ }
+ }
+ updateUrl =
+ latestRelease["assets"].asJsonArray[0].asJsonObject["browser_download_url"]
+ .asString
+ if (updateUrl.isNotEmpty()) {
+ if (ChattilsConfig.showUpdate) {
+ EssentialAPI.getNotifications().push(
+ Chattils.NAME,
+ "${Chattils.NAME} has a new update ($latestTag)! Click here to download it automatically!"
+ ) { EssentialAPI.getGuiUtil().openScreen(DownloadGui()) }
+ }
+ shouldUpdate = true
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ fun download(url: String, file: File): Boolean {
+ var url = url
+ if (file.exists()) return true
+ url = url.replace(" ", "%20")
+ try {
+ downloadToFile(url, file, "${Chattils.NAME}/${Chattils.VER}")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return false
+ }
+ return file.exists()
+ }
+
+ /**
+ * Adapted from RequisiteLaunchwrapper under LGPLv3
+ * https://github.com/Qalcyo/RequisiteLaunchwrapper/blob/main/LICENSE
+ */
+ fun addShutdownHook() {
+ Runtime.getRuntime().addShutdownHook(Thread {
+ println("Opening Deleter task...")
+ try {
+ val runtime = javaRuntime
+ if (Minecraft.isRunningOnMac) {
+ open(Chattils.jarFile.parentFile)
+ }
+ val file = File(Chattils.modDir.parentFile, "Deleter-1.2.jar")
+ Runtime.getRuntime()
+ .exec("\"" + runtime + "\" -jar \"" + file.absolutePath + "\" \"" + Chattils.jarFile.absolutePath + "\"")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ Thread.currentThread().interrupt()
+ })
+ }
+
+ /**
+ * Gets the current Java runtime being used.
+ *
+ * @link https://stackoverflow.com/a/47925649
+ */
+ @get:Throws(IOException::class)
+ val javaRuntime: String
+ get() {
+ val os = System.getProperty("os.name")
+ val java = System.getProperty("java.home") + File.separator + "bin" + File.separator +
+ if (os != null && os.lowercase().startsWith("windows")) "java.exe" else "java"
+ if (!File(java).isFile) {
+ throw IOException("Unable to find suitable java runtime at $java")
+ }
+ return java
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/utils/ImageTransferable.kt b/src/main/kotlin/cc/woverflow/chattils/utils/ImageTransferable.kt
new file mode 100644
index 0000000..b23e56e
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/utils/ImageTransferable.kt
@@ -0,0 +1,22 @@
+package cc.woverflow.chattils.utils
+
+import java.awt.Image
+import java.awt.datatransfer.DataFlavor
+import java.awt.datatransfer.Transferable
+import java.awt.datatransfer.UnsupportedFlavorException
+
+data class ImageTransferable(private val image: Image) : Transferable {
+
+ override fun getTransferDataFlavors(): Array<DataFlavor> {
+ return arrayOf(DataFlavor.imageFlavor)
+ }
+
+ override fun isDataFlavorSupported(flavor: DataFlavor?): Boolean {
+ return DataFlavor.imageFlavor.equals(flavor)
+ }
+
+ override fun getTransferData(flavor: DataFlavor?): Any {
+ if (isDataFlavorSupported(flavor)) return image
+ throw UnsupportedFlavorException(flavor)
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/utils/ListenableArrayList.kt b/src/main/kotlin/cc/woverflow/chattils/utils/ListenableArrayList.kt
new file mode 100644
index 0000000..3b270fd
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/utils/ListenableArrayList.kt
@@ -0,0 +1,9 @@
+package cc.woverflow.chattils.utils
+
+class ListenableArrayList<T>(private val runnable: (ListenableArrayList<T>) -> Unit, vararg elements: T): ArrayList<T>() {
+ override fun add(element: T): Boolean {
+ val value = super.add(element)
+ runnable.invoke(this)
+ return value
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/cc/woverflow/chattils/utils/ModCompatHooks.kt b/src/main/kotlin/cc/woverflow/chattils/utils/ModCompatHooks.kt
new file mode 100644
index 0000000..65c80f0
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/utils/ModCompatHooks.kt
@@ -0,0 +1,46 @@
+package cc.woverflow.chattils.utils
+
+import club.sk1er.patcher.config.PatcherConfig
+import com.llamalad7.betterchat.BetterChat
+import cc.woverflow.chattils.Chattils.isBetterChat
+import cc.woverflow.chattils.Chattils.isPatcher
+import cc.woverflow.chattils.config.ChattilsConfig.textRenderType
+import cc.woverflow.chattils.utils.RenderHelper.drawBorderedString
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.FontRenderer
+
+// 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 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): Int {
+ return when (textRenderType) {
+ 0 -> {
+ fontRenderer.drawString(text, x, y, color, false)
+ }
+ 2 -> {
+ drawBorderedString(fontRenderer, text, x.toInt(), y.toInt(), color)
+ }
+ else -> fontRenderer.drawString(text, x, y, color, true)
+ }
+ }
+}
diff --git a/src/main/kotlin/cc/woverflow/chattils/utils/RenderHelper.kt b/src/main/kotlin/cc/woverflow/chattils/utils/RenderHelper.kt
new file mode 100644
index 0000000..41325ca
--- /dev/null
+++ b/src/main/kotlin/cc/woverflow/chattils/utils/RenderHelper.kt
@@ -0,0 +1,248 @@
+package cc.woverflow.chattils.utils
+
+import cc.woverflow.chattils.config.ChattilsConfig
+import cc.woverflow.chattils.hook.GuiNewChatHook
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.FontRenderer
+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
+
+
+object RenderHelper {
+ private val regex = Regex("(?i)\\u00A7[0-9a-f]")
+ var bypassWyvtils = false
+ private set
+
+ /**
+ * 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 copyBufferedImageToClipboard(bufferedImage: BufferedImage) {
+ if (SystemUtils.IS_OS_WINDOWS) {
+ try {
+ val width = bufferedImage.width
+ val height = bufferedImage.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 = bufferedImage.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 = bufferedImage.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 =
+ bufferedImage.getRGB(0, 0, bufferedImage.width, bufferedImage.height, null, 0, bufferedImage.width)
+ val newImage = BufferedImage(bufferedImage.width, bufferedImage.height, BufferedImage.TYPE_INT_RGB)
+ newImage.setRGB(0, 0, newImage.width, newImage.height, pixels, 0, newImage.width)
+
+ try {
+ Toolkit.getDefaultToolkit().systemClipboard.setContents(ImageTransferable(bufferedImage), null)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+
+ /**
+ * Taken from https://github.com/Moulberry/HyChat
+ */
+ fun screenshotFramebuffer(framebuffer: Framebuffer, file: File): BufferedImage {
+ val w = framebuffer.framebufferWidth
+ val h = framebuffer.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(framebuffer.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 = framebuffer.framebufferTextureHeight - framebuffer.framebufferHeight
+ for (k in j until framebuffer.framebufferTextureHeight) {
+ for (l in 0 until framebuffer.framebufferWidth) {
+ bufferedimage.setRGB(l, k - j, pixelValues[k * framebuffer.framebufferTextureWidth + l])
+ }
+ }
+ if (ChattilsConfig.copyMode != 1) {
+ try {
+ file.parentFile.mkdirs()
+ ImageIO.write(bufferedimage, "png", file)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ return bufferedimage
+ }
+
+ /**
+ * 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
+ */
+ fun drawBorderedString(
+ fontRendererIn: FontRenderer,
+ text: String,
+ x: Int,
+ y: Int,
+ color: Int
+ ): Int {
+ val noColors = text.replace(regex, "\u00A7r")
+ var yes = 0
+ if (((Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatHook).textOpacity / 4) > 3) {
+ bypassWyvtils = true
+ for (xOff in -2..2) {
+ for (yOff in -2..2) {
+ if (xOff * xOff != yOff * yOff) {
+ yes += fontRendererIn.drawString(
+ noColors,
+ (xOff / 2f) + x, (yOff / 2f) + y, ((Minecraft.getMinecraft().ingameGUI.chatGUI as GuiNewChatHook).textOpacity / 4) shl 24, false
+ )
+ }
+ }
+ }
+ bypassWyvtils = false
+ }
+ yes += fontRendererIn.drawString(text, x, y, color)
+ return yes
+ }
+} \ No newline at end of file