diff options
author | Wyvest <wyvestbusiness@gmail.com> | 2023-11-22 08:18:19 +0900 |
---|---|---|
committer | Wyvest <wyvestbusiness@gmail.com> | 2023-11-22 08:18:19 +0900 |
commit | 8b373f577d9c6dde26357ef3fc86691f1efef9b4 (patch) | |
tree | a5328e995d8f4df21a9fe94ac8e384be08833c70 /src/main/kotlin/org/polyfrost | |
parent | 64230799777473246b5f98efbc596206c5bbf42d (diff) | |
download | Chatting-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')
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 |