From 994057e823c70e8da8ac46c536306b9a48c951e0 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sun, 26 Apr 2026 20:17:26 +0200 Subject: [PATCH 01/63] Add Chat-Bubbles Feature --- README.md | 16 + chattore/src/main/kotlin/Bubble.kt | 54 +++ chattore/src/main/kotlin/ChattORE.kt | 3 +- chattore/src/main/kotlin/ChattOREConfig.kt | 4 + chattore/src/main/kotlin/Messenger.kt | 75 +++- .../src/main/kotlin/feature/BubbleCommand.kt | 371 ++++++++++++++++++ chattore/src/main/kotlin/feature/Chat.kt | 6 +- 7 files changed, 520 insertions(+), 9 deletions(-) create mode 100644 chattore/src/main/kotlin/Bubble.kt create mode 100644 chattore/src/main/kotlin/feature/BubbleCommand.kt diff --git a/README.md b/README.md index d81b40e..ca67ebf 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,22 @@ Because we want to have a chat system that actually wOREks for us. | `/profile about ` | `chattore.profile.about` | Set your about | `/playerprofile` | | `/profile setabout ` | `chattore.profile.about.others` | Set another player's about | `/playerprofile` | +## Chat-Bubble Commands + +| Command | Permission | Description | Aliases | +|--------------------------------|--------------------------|-----------------------------------------------------------------|---------------------------| +| `/bubble create` | `chattore.bubble` | Create ("Blow") a Chat-Bubble | `/bb create\|/blow` | +| `/bubble invite ` | `chattore.bubble` | Invite Players to your (private) Chat-Bubble | `/bb invite` | +| `/bubble join ` | `chattore.bubble` | Join a Player's Chat-Bubble | `/bb join` | +| `/bubble leave` | `chattore.bubble` | Leave your current Chat-Bubble | `/bb leave` | +| `/bubble delete` | `chattore.bubble` | Delete ("Pop") your current Chat-Bubble | `/bb delete\|/pop` | +| `/bubble kick ` | `chattore.bubble` | Kick a Player from your own Chat-Bubble | `/bb kick` | +| `/bubble setPrivate ` | `chattore.bubble` | Set the visibility of your own Chat-Bubble | `/bb private` | +| `/bubble list` | `chattore.bubble` | List all Chat-Bubbles | `/bb list\|/bubbles` | +| `/shout ` | `chattore.bubble` | Send a message to global chat when inside a Chat-Bubble | No aliases | +| `/bubble forcedelete ` | `chattore.bubble.manage` | Force-Delete ("Burst") a specific Chat-Bubble | `/bb forcedelete\|/burst` | +| `seeGlobalChat ` | `chattore.bubble` | Toggles the visibility of global chat when inside a Chat-Bubble | `/gc` | + ## Other Commands | Command | Permission | Description | Aliases | diff --git a/chattore/src/main/kotlin/Bubble.kt b/chattore/src/main/kotlin/Bubble.kt new file mode 100644 index 0000000..d0c0685 --- /dev/null +++ b/chattore/src/main/kotlin/Bubble.kt @@ -0,0 +1,54 @@ +package org.openredstone.chattore + +import com.velocitypowered.api.proxy.Player +import java.util.UUID + +class Bubble(val players: MutableList, val invitedPlayers: MutableList, var visibility: Boolean) { + fun addPlayer(uuid: UUID): Boolean { + if (visibility) return players.add(uuid) + return if (invitedPlayers.contains(uuid)) players.add(uuid) && invitedPlayers.remove(uuid) + else false + } + + fun removePlayer(uuid: UUID): Boolean { + return players.remove(uuid) + } + + fun invitePlayer(uuid: UUID): Boolean { + return invitedPlayers.add(uuid) + } + + fun uninvitePlayer(uuid: UUID): Boolean { + return invitedPlayers.remove(uuid) + } +} + +val bubbles: MutableMap = mutableMapOf() +var id: Int = 0 + +class BubbleManager() { + fun getBubbles(): Map { + return bubbles + } + + fun createBubble(player: UUID) { + bubbles[id] = Bubble(mutableListOf(player), mutableListOf(), true) + id += 1 + } + + fun removeBubble(id: Int) { + bubbles.remove(id) + } + + fun getBubbleId(player: UUID): Int? { + return bubbles.entries.firstOrNull{it.value.players.contains(player)}?.key + } + + fun getBubble(id: Int): Bubble? { + return bubbles[id] + } + + fun getBubbleByPlayer(player: Player): Bubble? { + return bubbles.entries.firstOrNull{it.value.players.contains(player.uniqueId)}?.value + } +} \ No newline at end of file diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 5029a10..dc4bd08 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -46,13 +46,14 @@ class ChattORE @Inject constructor( } pluginScope.apply { val emojis = createEmojiFeature() - val messenger = createMessenger(emojis, database, luckPerms, config.format.global) + val messenger = createMessenger(emojis, database, luckPerms, config.format) val userCache = createUserCache(database.database) createAliasFeature() createChatFeature( messenger, ChatConfirmationConfig(config.regexes), ) + createBubbleFeature(messenger, config.format, userCache) createChattoreFeature() createDiscordFeature(messenger, emojis, config.discord) createFunCommandsFeature() diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index 5c348d8..2530d0c 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -18,6 +18,10 @@ data class FormatConfig( val global: String = " | : ", val join: String = " has joined the network", val leave: String = " has left the network", + val bubbleChatBubble: String = "[B] | : ", + val bubbleHeading: String = "Bubbles:", + val bubbleList: String = "", + val bubbleInfoMessage: String = "[Bubbles] ", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", ) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 7c01e79..2c8a130 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -14,17 +14,18 @@ import org.openredstone.chattore.feature.DiscordBroadcastEvent import org.openredstone.chattore.feature.Emojis import org.openredstone.chattore.feature.NickPreset import java.net.URI +import java.util.UUID fun PluginScope.createMessenger( emojis: Emojis, database: Storage, luckPerms: LuckPerms, - chatBroadcastFormat: String, + formatConfig: FormatConfig, ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, chatBroadcastFormat, fileTypeMap) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) } class Messenger( @@ -32,7 +33,7 @@ class Messenger( private val proxy: ProxyServer, private val database: Storage, private val luckPerms: LuckPerms, - private val chatBroadcastFormat: String, + private val formatConfig: FormatConfig, private val fileTypeMap: Map>, ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -45,6 +46,12 @@ class Messenger( buildEmojiReplacement(emojis), ) + val bubbleManager = BubbleManager() + private val seeGlobalChat: MutableList = mutableListOf() + private var excludedCache: Set = emptySet() + private var cacheDirty: Boolean = true + + private fun formatReplacement(key: String, tag: String): TextReplacementConfig = TextReplacementConfig.builder() .match("""((\\?)(${Regex.escape(key)}(.*?)${Regex.escape(key)}))""") @@ -80,12 +87,17 @@ class Messenger( ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) val compoPrefix = prefix.legacyDeserialize() - proxy.all.sendRichMessage( - chatBroadcastFormat, + + if (cacheDirty) { + rebuildExcludedCache() + } + + proxy.allPlayers.filter { it.uniqueId !in excludedCache }.forEach { it.sendRichMessage( + formatConfig.global, "message" toC prepareChatMessage(message, player), "sender" toC sender, - "prefix" toC compoPrefix, - ) + "prefix" toC compoPrefix) + } val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix) val discordBroadcast = DiscordBroadcastEvent( @@ -97,6 +109,33 @@ class Messenger( proxy.eventManager.fireAndForget(discordBroadcast) } + fun broadcastBubbleMessage(player: Player, message: String) { + val userId = player.uniqueId + val userManager = luckPerms.userManager + val luckUser = userManager.getUser(userId) ?: return + val name = database.getNickname(userId) ?: NickPreset(player.username) + val sender = + "Click for more'>" + .renderSimpleC(name.render(player.username)) + + val prefix = luckUser.cachedData.metaData.prefix + ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) + val compoPrefix = prefix.legacyDeserialize() + + val bubble = bubbleManager.getBubbleByPlayer(player) + + bubble?.players?.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + target.sendRichMessage( + formatConfig.bubbleChatBubble, + "message" toC prepareChatMessage(message, player), + "sender" toC sender, + "prefix" toC compoPrefix, + ) + } + } + fun prepareChatMessage( message: String, player: Player?, @@ -145,6 +184,28 @@ class Messenger( "").render() } + fun rebuildExcludedCache() { + val bubbleExcluded = bubbleManager.getBubbles().values.flatMap { it.players }.toMutableSet() + bubbleExcluded.removeAll(seeGlobalChat) + + excludedCache = bubbleExcluded + cacheDirty = false + } + + fun setCacheDirty() { + cacheDirty = true + } + + fun addGlobalOverride(uuid: UUID) { + seeGlobalChat.add(uuid) + cacheDirty = true + } + + fun removeGlobalOverride(uuid: UUID) { + seeGlobalChat.remove(uuid) + cacheDirty = true + } + private fun Component.performReplacements(replacements: List): Component = replacements.fold(this, Component::replaceText) } diff --git a/chattore/src/main/kotlin/feature/BubbleCommand.kt b/chattore/src/main/kotlin/feature/BubbleCommand.kt new file mode 100644 index 0000000..36156ec --- /dev/null +++ b/chattore/src/main/kotlin/feature/BubbleCommand.kt @@ -0,0 +1,371 @@ +package org.openredstone.chattore.feature + +import co.aikar.commands.BaseCommand +import co.aikar.commands.annotation.CommandAlias +import co.aikar.commands.annotation.CommandCompletion +import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Single +import co.aikar.commands.annotation.Subcommand +import com.velocitypowered.api.proxy.Player +import com.velocitypowered.api.proxy.ProxyServer +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent +import org.openredstone.chattore.* + +fun PluginScope.createBubbleFeature(messenger: Messenger, formatConfig: FormatConfig, userCache: UserCache) { + registerCommands(BubbleCommand(messenger, proxy, formatConfig, userCache)) +} + +@CommandAlias("bubble|bb") +@CommandPermission("chattore.bubble") +private class BubbleCommand( + private val messenger: Messenger, + private val proxy: ProxyServer, + private val formatConfig: FormatConfig, + private var userCache: UserCache, +) : BaseCommand() { + @Subcommand("create") + @CommandAlias("blow") + fun create(sender: Player) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble != null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You are already in a Chat-Bubble!") + ) + return + } + messenger.bubbleManager.createBubble(sender.uniqueId) + messenger.setCacheDirty() + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("Successfully created Chat-Bubble!") + ) + } + + @Subcommand("invite") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + fun invite(sender: Player, @Single target: String) { + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble?.invitePlayer(player.uniqueId) == true) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("Successfully invited ${player.username}!") + ) + player.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("${sender.username} invited you to their Chat-Bubble!") + ) + } else { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("Could not invite ${player.username}!") + ) + } + } + + @Subcommand("join") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + fun join(sender: Player, @Single target: String) { + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + val senderBubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (senderBubble != null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You are already in a Chat-Bubble!") + ) + return + } + val bubble = messenger.bubbleManager.getBubbleByPlayer(player) + if (bubble?.addPlayer(sender.uniqueId) == true) { + messenger.setCacheDirty() + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You successfully joined ${player.username}'s Chat-Bubble!") + ) + } else { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("${sender.username} joined your Chat-Bubble!") + ) + } + } + } else { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You were not invited to ${player.username}'s Chat-Bubble!") + ) + } + } + + @Subcommand("leave") + fun leave(sender: Player) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble == null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You are not in a Chat-Bubble!") + ) + return + } + val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + if (bubble.removePlayer(sender.uniqueId)) { + messenger.setCacheDirty() + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You successfully left the Chat-Bubble!") + ) + if (bubble.players.isEmpty()) { + messenger.bubbleManager.removeBubble(bubbleId) + return + } + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("${sender.username} left your Chat-Bubble!") + ) + } + } else { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("Could not leave Chat-Bubble!") + ) + } + } + + @Subcommand("delete") + @CommandAlias("pop") + fun delete(sender: Player) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble == null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You are not in a Chat-Bubble!") + ) + return + } else if (bubble.players.first() != sender.uniqueId) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You cannot pop this Chat-Bubble!") + ) + return + } + val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You popped the Chat-Bubble!") + ) + } else { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("${sender.username} popped your Chat-Bubble!") + ) + } + } + messenger.bubbleManager.removeBubble(bubbleId) + messenger.setCacheDirty() + } + + @Subcommand("kick") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + fun kick(sender: Player, @Single target: String) { + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble == null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You are not in a Chat-Bubble!") + ) + return + } else if (bubble.players.first() != sender.uniqueId) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You cannot kick players from this Chat-Bubble!") + ) + return + } + if (bubble.removePlayer(player.uniqueId)) { + messenger.setCacheDirty() + player.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You were kicked out of the Chat-Bubble!") + ) + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You kicked ${player.username} from the Chat-Bubble!") + ) + } else { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("${sender.username} kicked ${player.username} from your Chat-Bubble!") + ) + } + } + } else { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("Could not leave Chat-Bubble!") + ) + } + } + + @Subcommand("setPrivate") + @CommandCompletion("true|false") + fun setPrivate(sender: Player, boolean: Boolean) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble == null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You are not in a Chat-Bubble!") + ) + return + } else if (bubble.players.first() != sender.uniqueId) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You cannot change the visibility of this Chat-Bubble!") + ) + return + } + + bubble.visibility = boolean + + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You set the visibility of the Chat-Bubble to $boolean!") + ) + } else { + target.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("${sender.username} set the visibility of the Chat-Bubble to $boolean!") + ) + } + } + } + + @Subcommand("list") + fun list(sender: Player) { + if (messenger.bubbleManager.getBubbles().isEmpty()) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("There are currently no Chat-Bubbles!") + ) + return + } + + sender.sendRichMessage(formatConfig.bubbleHeading) + + for (bubble in messenger.bubbleManager.getBubbles().values) { + val names = mutableListOf() + + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + names.add(target.username) + } + + val playersString = names.joinToString(", ") + + val firstPlayer = bubble.players.firstOrNull()?.let { uuid -> + proxy.getPlayer(uuid).orElse(null)?.username + } + + val string = "$playersString | [Join]" + + val component = if (firstPlayer != null) { + Component.text(string) + .clickEvent( + ClickEvent.suggestCommand("/bubble join $firstPlayer") + ) + .hoverEvent( + HoverEvent.showText(Component.text("Click to join bubble")) + ) + } else { + Component.text(string) + } + + sender.sendRichMessage( + "", + "component" toC component + ) + } + } + + @Subcommand("forcedelete") + @CommandAlias("burst") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + @CommandPermission("chattore.bubble.manage") + fun forcedelete(sender: Player, @Single target: String) { + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + val bubble = messenger.bubbleManager.getBubbleByPlayer(player) + if (bubble == null) { + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("Target is not in a Chat-Bubble!") + ) + return + } + + val bubbleId = messenger.bubbleManager.getBubbleId(player.uniqueId)!! + messenger.bubbleManager.removeBubble(bubbleId) + messenger.setCacheDirty() + + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You successfully burst ${player.username}'s Chat-Bubble!") + ) + } + + @CommandAlias("shout") + fun shout(sender: Player, message: String) { + if (message != "") messenger.broadcastChatMessage(sender.currentServer.get().serverInfo.name, sender, message) + } + + @CommandAlias("seeglobalchat|gc") + @CommandCompletion("true|false") + fun seeGlobalChat(sender: Player, boolean: Boolean) { + if (boolean) { + messenger.addGlobalOverride(sender.uniqueId) + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You will now see global chat inside your Chat-Bubble!") + ) + return + } + messenger.removeGlobalOverride(sender.uniqueId) + sender.sendRichMessage( + formatConfig.bubbleInfoMessage, + "message" toC Component.text("You won't see global chat inside your Chat-Bubble anymore!") + ) + } +} diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index da8a982..b64bb60 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -48,7 +48,11 @@ private class ChatListener( if (isFlagged(player, message)) return logger.info("${player.username} (${player.uniqueId}): $message") player.currentServer.ifPresent { server -> - messenger.broadcastChatMessage(server.serverInfo.name, player, message) + if (messenger.bubbleManager.getBubbleByPlayer(player) != null) { + messenger.broadcastBubbleMessage(player, message) + } else { + messenger.broadcastChatMessage(server.serverInfo.name, player, message) + } } } From 50c092363d9763b273c20d7f1adb77fae7fa4f8f Mon Sep 17 00:00:00 2001 From: Waffle <151867877+Wueffi@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:46:01 +0200 Subject: [PATCH 02/63] Remove unnecessary (mb) line --- chattore/src/main/kotlin/ChattOREConfig.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index 2530d0c..0518a7f 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -20,7 +20,6 @@ data class FormatConfig( val leave: String = " has left the network", val bubbleChatBubble: String = "[B] | : ", val bubbleHeading: String = "Bubbles:", - val bubbleList: String = "", val bubbleInfoMessage: String = "[Bubbles] ", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", From 419034bf96bba01926e0b772b84604f85c58db11 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 27 Apr 2026 15:50:26 +0200 Subject: [PATCH 03/63] Ready for another review round! --- chattore/src/main/kotlin/Bubble.kt | 54 --- chattore/src/main/kotlin/ChattORE.kt | 2 +- chattore/src/main/kotlin/ChattOREConfig.kt | 2 - chattore/src/main/kotlin/Messenger.kt | 39 +- chattore/src/main/kotlin/Storage.kt | 7 + chattore/src/main/kotlin/feature/Bubble.kt | 336 ++++++++++++++++ .../src/main/kotlin/feature/BubbleCommand.kt | 371 ------------------ 7 files changed, 361 insertions(+), 450 deletions(-) delete mode 100644 chattore/src/main/kotlin/Bubble.kt create mode 100644 chattore/src/main/kotlin/feature/Bubble.kt delete mode 100644 chattore/src/main/kotlin/feature/BubbleCommand.kt diff --git a/chattore/src/main/kotlin/Bubble.kt b/chattore/src/main/kotlin/Bubble.kt deleted file mode 100644 index d0c0685..0000000 --- a/chattore/src/main/kotlin/Bubble.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.openredstone.chattore - -import com.velocitypowered.api.proxy.Player -import java.util.UUID - -class Bubble(val players: MutableList, val invitedPlayers: MutableList, var visibility: Boolean) { - fun addPlayer(uuid: UUID): Boolean { - if (visibility) return players.add(uuid) - return if (invitedPlayers.contains(uuid)) players.add(uuid) && invitedPlayers.remove(uuid) - else false - } - - fun removePlayer(uuid: UUID): Boolean { - return players.remove(uuid) - } - - fun invitePlayer(uuid: UUID): Boolean { - return invitedPlayers.add(uuid) - } - - fun uninvitePlayer(uuid: UUID): Boolean { - return invitedPlayers.remove(uuid) - } -} - -val bubbles: MutableMap = mutableMapOf() -var id: Int = 0 - -class BubbleManager() { - fun getBubbles(): Map { - return bubbles - } - - fun createBubble(player: UUID) { - bubbles[id] = Bubble(mutableListOf(player), mutableListOf(), true) - id += 1 - } - - fun removeBubble(id: Int) { - bubbles.remove(id) - } - - fun getBubbleId(player: UUID): Int? { - return bubbles.entries.firstOrNull{it.value.players.contains(player)}?.key - } - - fun getBubble(id: Int): Bubble? { - return bubbles[id] - } - - fun getBubbleByPlayer(player: Player): Bubble? { - return bubbles.entries.firstOrNull{it.value.players.contains(player.uniqueId)}?.value - } -} \ No newline at end of file diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index dc4bd08..9493280 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -53,7 +53,7 @@ class ChattORE @Inject constructor( messenger, ChatConfirmationConfig(config.regexes), ) - createBubbleFeature(messenger, config.format, userCache) + createBubbleFeature(messenger, userCache, database) createChattoreFeature() createDiscordFeature(messenger, emojis, config.discord) createFunCommandsFeature() diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index 0518a7f..9ce5bbb 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -19,8 +19,6 @@ data class FormatConfig( val join: String = " has joined the network", val leave: String = " has left the network", val bubbleChatBubble: String = "[B] | : ", - val bubbleHeading: String = "Bubbles:", - val bubbleInfoMessage: String = "[Bubbles] ", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", ) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 2c8a130..8e60a51 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -10,6 +10,7 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextReplacementConfig import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms +import org.openredstone.chattore.feature.BubbleManager import org.openredstone.chattore.feature.DiscordBroadcastEvent import org.openredstone.chattore.feature.Emojis import org.openredstone.chattore.feature.NickPreset @@ -47,11 +48,9 @@ class Messenger( ) val bubbleManager = BubbleManager() - private val seeGlobalChat: MutableList = mutableListOf() private var excludedCache: Set = emptySet() private var cacheDirty: Boolean = true - private fun formatReplacement(key: String, tag: String): TextReplacementConfig = TextReplacementConfig.builder() .match("""((\\?)(${Regex.escape(key)}(.*?)${Regex.escape(key)}))""") @@ -83,20 +82,19 @@ class Messenger( "Click for more'>" .renderSimpleC(name.render(player.username)) - val prefix = luckUser.cachedData.metaData.prefix - ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) - - val compoPrefix = prefix.legacyDeserialize() + val compoPrefix = getPrefixComponent(userId, player) if (cacheDirty) { rebuildExcludedCache() } - proxy.allPlayers.filter { it.uniqueId !in excludedCache }.forEach { it.sendRichMessage( - formatConfig.global, - "message" toC prepareChatMessage(message, player), - "sender" toC sender, - "prefix" toC compoPrefix) + proxy.allPlayers.filter { it.uniqueId !in excludedCache }.forEach { + it.sendRichMessage( + formatConfig.global, + "message" toC prepareChatMessage(message, player), + "sender" toC sender, + "prefix" toC compoPrefix + ) } val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix) @@ -118,9 +116,7 @@ class Messenger( "Click for more'>" .renderSimpleC(name.render(player.username)) - val prefix = luckUser.cachedData.metaData.prefix - ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) - val compoPrefix = prefix.legacyDeserialize() + val compoPrefix = getPrefixComponent(userId, player) val bubble = bubbleManager.getBubbleByPlayer(player) @@ -186,7 +182,7 @@ class Messenger( fun rebuildExcludedCache() { val bubbleExcluded = bubbleManager.getBubbles().values.flatMap { it.players }.toMutableSet() - bubbleExcluded.removeAll(seeGlobalChat) + bubbleExcluded.removeAll(database.getAllGlobalChatEnabled()) excludedCache = bubbleExcluded cacheDirty = false @@ -196,14 +192,13 @@ class Messenger( cacheDirty = true } - fun addGlobalOverride(uuid: UUID) { - seeGlobalChat.add(uuid) - cacheDirty = true - } + private fun getPrefixComponent(userId: UUID, player: Player): Component { + val luckUser = luckPerms.userManager.getUser(userId) ?: return Component.empty() - fun removeGlobalOverride(uuid: UUID) { - seeGlobalChat.remove(uuid) - cacheDirty = true + val prefix = luckUser.cachedData.metaData.prefix + ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) + + return prefix.legacyDeserialize() } private fun Component.performReplacements(replacements: List): Component = diff --git a/chattore/src/main/kotlin/Storage.kt b/chattore/src/main/kotlin/Storage.kt index 24c2b13..b7a47b1 100644 --- a/chattore/src/main/kotlin/Storage.kt +++ b/chattore/src/main/kotlin/Storage.kt @@ -6,6 +6,7 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import org.openredstone.chattore.feature.MailboxItem import org.openredstone.chattore.feature.NickPreset +import org.openredstone.chattore.feature.seeGlobalChatEnabled import java.nio.file.Path import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -153,4 +154,10 @@ class Storage( val jsonString = result[JsonSetting.value] Json.decodeFromString(jsonString) } + + fun getAllGlobalChatEnabled(): Set = transaction(database) { + JsonSetting.selectAll().where { + (JsonSetting.key eq seeGlobalChatEnabled.key) and (JsonSetting.value eq Json.encodeToString(true)) + }.map { UUID.fromString(it[JsonSetting.uuid]) }.toSet() + } } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt new file mode 100644 index 0000000..059b7fd --- /dev/null +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -0,0 +1,336 @@ +package org.openredstone.chattore.feature + +import co.aikar.commands.BaseCommand +import co.aikar.commands.annotation.CommandAlias +import co.aikar.commands.annotation.CommandCompletion +import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Single +import co.aikar.commands.annotation.Subcommand +import com.velocitypowered.api.proxy.Player +import com.velocitypowered.api.proxy.ProxyServer +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent +import org.openredstone.chattore.* +import java.util.UUID + +val seeGlobalChatEnabled = Setting("seeGlobalChat") + +fun PluginScope.createBubbleFeature(messenger: Messenger, userCache: UserCache, database: Storage) { + registerCommands(BubbleCommand(messenger, proxy, userCache, database)) +} + +@CommandAlias("bubble|bb") +@CommandPermission("chattore.bubble") +private class BubbleCommand( + private val messenger: Messenger, + private val proxy: ProxyServer, + private var userCache: UserCache, + private var database: Storage +) : BaseCommand() { + + private val Player.seeGlobalChat: Boolean get() = database.getSetting(seeGlobalChatEnabled, uniqueId) ?: false + + @Subcommand("create") + @CommandAlias("blow") + fun create(sender: Player) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (bubble != null) throw ChattoreException("You are already in a Chat-Bubble!") + messenger.bubbleManager.createBubble(sender.uniqueId) + messenger.setCacheDirty() + sender.sendInfo("Successfully created Chat-Bubble!") + } + + @Subcommand("invite") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + fun invite(sender: Player, @Single target: String) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + ?: throw ChattoreException("You are not in a Chat-Bubble!") + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + + if (bubble.isInvited(player.uniqueId)) { + sender.sendInfo("${player.username} is already invited!") + return + } else if (bubble.containsPlayer(player.uniqueId)) { + sender.sendInfo("${player.username} is already in the Chat-Bubble!") + return + } + + bubble.invitePlayer(player.uniqueId) + sender.sendInfo("Successfully invited ${player.username}!") + player.sendInfo("${sender.username} invited you to their Chat-Bubble!") + + } + + @Subcommand("join") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + fun join(sender: Player, @Single target: String) { + val senderBubble = messenger.bubbleManager.getBubbleByPlayer(sender) + if (senderBubble != null) { + sender.sendInfo("You are already in a Chat-Bubble!") + return + } + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + val bubble = messenger.bubbleManager.getBubbleByPlayer(player) + ?: throw ChattoreException("Target is not in a Chat-Bubble!") + + if (!bubble.isInvited(player.uniqueId) && bubble.isPrivate) { + sender.sendInfo("You were not invited to the Chat-Bubble!") + return + } + + bubble.addPlayer(sender.uniqueId) + messenger.setCacheDirty() + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendInfo("You successfully joined ${player.username}'s Chat-Bubble!") + } else { + target.sendInfo("${sender.username} joined your Chat-Bubble!") + } + } + } + + @Subcommand("leave") + fun leave(sender: Player) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + ?: throw ChattoreException("You are not in a Chat-Bubble!") + val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + bubble.removePlayer(sender.uniqueId) + messenger.setCacheDirty() + sender.sendInfo("You successfully left the Chat-Bubble!") + if (bubble.players.isEmpty()) { + messenger.bubbleManager.removeBubble(bubbleId) + return + } + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + target.sendInfo("${sender.username} left your Chat-Bubble!") + } + } + + @Subcommand("delete") + @CommandAlias("pop") + fun delete(sender: Player) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + ?: throw ChattoreException("You are not in a Chat-Bubble!") + + if (!bubble.isOwner(sender.uniqueId)) { + val ownerName = proxy.getPlayer(bubble.owner) + sender.sendInfo("You are not the owner of this Chat-Bubble! (${ownerName} is.)") + return + } + val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendInfo("You popped the Chat-Bubble!") + } else { + target.sendInfo("${sender.username} popped your Chat-Bubble!") + } + } + messenger.bubbleManager.removeBubble(bubbleId) + messenger.setCacheDirty() + } + + @Subcommand("kick") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + fun kick(sender: Player, @Single target: String) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + ?: throw ChattoreException("You are not in a Chat-Bubble!") + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + + if (player.uniqueId == sender.uniqueId) { + sender.sendInfo("You cannot kick yourself!") + return + } else if (!bubble.isOwner(sender.uniqueId)) { + val ownerName = proxy.getPlayer(bubble.owner) + sender.sendInfo("You are not the owner of this Chat-Bubble! (${ownerName} is.)") + return + } + + bubble.removePlayer(player.uniqueId) + messenger.setCacheDirty() + player.sendInfo("You were kicked out of the Chat-Bubble!") + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendInfo("You kicked ${player.username} from the Chat-Bubble!") + } else { + target.sendInfo("${sender.username} kicked ${player.username} from your Chat-Bubble!") + } + } + } + + @Subcommand("setPrivate") + @CommandCompletion("true|false") + fun setPrivate(sender: Player, boolean: Boolean) { + val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) + ?: throw ChattoreException("You are not in a Chat-Bubble!") + if (!bubble.isOwner(sender.uniqueId)) { + val ownerName = proxy.getPlayer(bubble.owner) + sender.sendInfo("You are not the creator of this Chat-Bubble! (${ownerName} is.)") + return + } + + bubble.isPrivate = boolean + + bubble.players.forEach { uuid -> + val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + + if (target == sender) { + target.sendInfo("The Chat-Bubble is now ${if (boolean) "private" else "public"}!") + } else { + target.sendInfo("${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!") + } + } + } + + @Subcommand("list") + fun list(sender: Player) { + if (messenger.bubbleManager.getBubbles().isEmpty()) { + sender.sendInfo("There are currently no Chat-Bubbles!") + return + } + + sender.sendRichMessage("Bubbles:") + + for (bubble in messenger.bubbleManager.getBubbles().values) { + val playersString = bubble.players + .mapNotNull { uuid -> proxy.getPlayer(uuid).orElse(null)?.username } + .joinToString(", ") + + val firstPlayer = bubble.players.firstOrNull()?.let { uuid -> + proxy.getPlayer(uuid).orElse(null)?.username + } + + val string = "$playersString | [Join]" + + val component = Component.text(string) + .clickEvent( + ClickEvent.suggestCommand("/bubble join $firstPlayer") + ) + .hoverEvent( + HoverEvent.showText(Component.text("Click to join bubble")) + ) + sender.sendMessage(component) + } + } + + @Subcommand("forcedelete") + @CommandAlias("burst") + @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") + @CommandPermission("chattore.bubble.manage") + fun forcedelete(sender: Player, @Single target: String) { + val targetUuid = userCache.uuidOrNull(target) + ?: throw ChattoreException("We do not recognize that user!") + val player: Player = proxy.getPlayer(targetUuid) + .orElseThrow { ChattoreException("User is not online!") } + val bubble = messenger.bubbleManager.getBubbleByPlayer(player) + if (bubble == null) { + sender.sendInfo("Target is not in a Chat-Bubble!") + return + } + + val bubbleId = messenger.bubbleManager.getBubbleId(player.uniqueId)!! + messenger.bubbleManager.removeBubble(bubbleId) + messenger.setCacheDirty() + + sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") + } + + @CommandAlias("shout") + fun shout(sender: Player, message: String) { + messenger.broadcastChatMessage(sender.currentServer.get().serverInfo.name, sender, message) + } + + @CommandAlias("seeglobalchat|gc") + @CommandCompletion("true|false") + fun seeGlobalChat(sender: Player, boolean: Boolean) { + database.setSetting(seeGlobalChatEnabled, sender.uniqueId, boolean) + messenger.setCacheDirty() + if (boolean) { + sender.sendInfo("You will now see global chat inside your Chat-Bubble!") + return + } + sender.sendInfo("You won't see global chat inside your Chat-Bubble anymore!") + } +} + +data class Bubble( + val owner: UUID, + val players: MutableSet, + val invitedPlayers: MutableSet, + var isPrivate: Boolean +) { + fun addPlayer(uuid: UUID): Boolean { + if (!isPrivate) return players.add(uuid) + + if (uuid !in invitedPlayers) return false + + invitedPlayers.remove(uuid) + return players.add(uuid) + } + + fun removePlayer(uuid: UUID): Boolean { + return players.remove(uuid) + } + + fun invitePlayer(uuid: UUID): Boolean { + return invitedPlayers.add(uuid) + } + + fun containsPlayer(uuid: UUID): Boolean { + return players.contains(uuid) + } + + fun isInvited(uuid: UUID): Boolean { + return invitedPlayers.contains(uuid) + } + + fun isOwner(uuid: UUID): Boolean { + return owner == uuid + } +} + +class BubbleManager { + val bubbles: MutableMap = mutableMapOf() + var id: Int = 0 + + fun getBubbles(): Map { + return bubbles + } + + fun createBubble(player: UUID) { + bubbles[id] = Bubble(player, mutableSetOf(player), mutableSetOf(), false) + id += 1 + } + + fun removeBubble(id: Int) { + bubbles.remove(id) + } + + fun getBubbleId(player: UUID): Int? { + return bubbles.entries.firstOrNull{it.value.players.contains(player)}?.key + } + + fun getBubble(id: Int): Bubble? { + return bubbles[id] + } + + fun getBubbleByPlayer(player: Player): Bubble? { + return bubbles.entries.firstOrNull{it.value.players.contains(player.uniqueId)}?.value + } +} \ No newline at end of file diff --git a/chattore/src/main/kotlin/feature/BubbleCommand.kt b/chattore/src/main/kotlin/feature/BubbleCommand.kt deleted file mode 100644 index 36156ec..0000000 --- a/chattore/src/main/kotlin/feature/BubbleCommand.kt +++ /dev/null @@ -1,371 +0,0 @@ -package org.openredstone.chattore.feature - -import co.aikar.commands.BaseCommand -import co.aikar.commands.annotation.CommandAlias -import co.aikar.commands.annotation.CommandCompletion -import co.aikar.commands.annotation.CommandPermission -import co.aikar.commands.annotation.Single -import co.aikar.commands.annotation.Subcommand -import com.velocitypowered.api.proxy.Player -import com.velocitypowered.api.proxy.ProxyServer -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.event.ClickEvent -import net.kyori.adventure.text.event.HoverEvent -import org.openredstone.chattore.* - -fun PluginScope.createBubbleFeature(messenger: Messenger, formatConfig: FormatConfig, userCache: UserCache) { - registerCommands(BubbleCommand(messenger, proxy, formatConfig, userCache)) -} - -@CommandAlias("bubble|bb") -@CommandPermission("chattore.bubble") -private class BubbleCommand( - private val messenger: Messenger, - private val proxy: ProxyServer, - private val formatConfig: FormatConfig, - private var userCache: UserCache, -) : BaseCommand() { - @Subcommand("create") - @CommandAlias("blow") - fun create(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble != null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You are already in a Chat-Bubble!") - ) - return - } - messenger.bubbleManager.createBubble(sender.uniqueId) - messenger.setCacheDirty() - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("Successfully created Chat-Bubble!") - ) - } - - @Subcommand("invite") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun invite(sender: Player, @Single target: String) { - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble?.invitePlayer(player.uniqueId) == true) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("Successfully invited ${player.username}!") - ) - player.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("${sender.username} invited you to their Chat-Bubble!") - ) - } else { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("Could not invite ${player.username}!") - ) - } - } - - @Subcommand("join") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun join(sender: Player, @Single target: String) { - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val senderBubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (senderBubble != null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You are already in a Chat-Bubble!") - ) - return - } - val bubble = messenger.bubbleManager.getBubbleByPlayer(player) - if (bubble?.addPlayer(sender.uniqueId) == true) { - messenger.setCacheDirty() - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You successfully joined ${player.username}'s Chat-Bubble!") - ) - } else { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("${sender.username} joined your Chat-Bubble!") - ) - } - } - } else { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You were not invited to ${player.username}'s Chat-Bubble!") - ) - } - } - - @Subcommand("leave") - fun leave(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble == null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You are not in a Chat-Bubble!") - ) - return - } - val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! - if (bubble.removePlayer(sender.uniqueId)) { - messenger.setCacheDirty() - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You successfully left the Chat-Bubble!") - ) - if (bubble.players.isEmpty()) { - messenger.bubbleManager.removeBubble(bubbleId) - return - } - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("${sender.username} left your Chat-Bubble!") - ) - } - } else { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("Could not leave Chat-Bubble!") - ) - } - } - - @Subcommand("delete") - @CommandAlias("pop") - fun delete(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble == null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You are not in a Chat-Bubble!") - ) - return - } else if (bubble.players.first() != sender.uniqueId) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You cannot pop this Chat-Bubble!") - ) - return - } - val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You popped the Chat-Bubble!") - ) - } else { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("${sender.username} popped your Chat-Bubble!") - ) - } - } - messenger.bubbleManager.removeBubble(bubbleId) - messenger.setCacheDirty() - } - - @Subcommand("kick") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun kick(sender: Player, @Single target: String) { - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble == null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You are not in a Chat-Bubble!") - ) - return - } else if (bubble.players.first() != sender.uniqueId) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You cannot kick players from this Chat-Bubble!") - ) - return - } - if (bubble.removePlayer(player.uniqueId)) { - messenger.setCacheDirty() - player.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You were kicked out of the Chat-Bubble!") - ) - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You kicked ${player.username} from the Chat-Bubble!") - ) - } else { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("${sender.username} kicked ${player.username} from your Chat-Bubble!") - ) - } - } - } else { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("Could not leave Chat-Bubble!") - ) - } - } - - @Subcommand("setPrivate") - @CommandCompletion("true|false") - fun setPrivate(sender: Player, boolean: Boolean) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble == null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You are not in a Chat-Bubble!") - ) - return - } else if (bubble.players.first() != sender.uniqueId) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You cannot change the visibility of this Chat-Bubble!") - ) - return - } - - bubble.visibility = boolean - - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You set the visibility of the Chat-Bubble to $boolean!") - ) - } else { - target.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("${sender.username} set the visibility of the Chat-Bubble to $boolean!") - ) - } - } - } - - @Subcommand("list") - fun list(sender: Player) { - if (messenger.bubbleManager.getBubbles().isEmpty()) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("There are currently no Chat-Bubbles!") - ) - return - } - - sender.sendRichMessage(formatConfig.bubbleHeading) - - for (bubble in messenger.bubbleManager.getBubbles().values) { - val names = mutableListOf() - - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - names.add(target.username) - } - - val playersString = names.joinToString(", ") - - val firstPlayer = bubble.players.firstOrNull()?.let { uuid -> - proxy.getPlayer(uuid).orElse(null)?.username - } - - val string = "$playersString | [Join]" - - val component = if (firstPlayer != null) { - Component.text(string) - .clickEvent( - ClickEvent.suggestCommand("/bubble join $firstPlayer") - ) - .hoverEvent( - HoverEvent.showText(Component.text("Click to join bubble")) - ) - } else { - Component.text(string) - } - - sender.sendRichMessage( - "", - "component" toC component - ) - } - } - - @Subcommand("forcedelete") - @CommandAlias("burst") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - @CommandPermission("chattore.bubble.manage") - fun forcedelete(sender: Player, @Single target: String) { - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(player) - if (bubble == null) { - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("Target is not in a Chat-Bubble!") - ) - return - } - - val bubbleId = messenger.bubbleManager.getBubbleId(player.uniqueId)!! - messenger.bubbleManager.removeBubble(bubbleId) - messenger.setCacheDirty() - - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You successfully burst ${player.username}'s Chat-Bubble!") - ) - } - - @CommandAlias("shout") - fun shout(sender: Player, message: String) { - if (message != "") messenger.broadcastChatMessage(sender.currentServer.get().serverInfo.name, sender, message) - } - - @CommandAlias("seeglobalchat|gc") - @CommandCompletion("true|false") - fun seeGlobalChat(sender: Player, boolean: Boolean) { - if (boolean) { - messenger.addGlobalOverride(sender.uniqueId) - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You will now see global chat inside your Chat-Bubble!") - ) - return - } - messenger.removeGlobalOverride(sender.uniqueId) - sender.sendRichMessage( - formatConfig.bubbleInfoMessage, - "message" toC Component.text("You won't see global chat inside your Chat-Bubble anymore!") - ) - } -} From 378d0342624a8b426275e6d53024eddfc263bc59 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 27 Apr 2026 19:18:53 +0200 Subject: [PATCH 04/63] Changes! --- chattore/src/main/kotlin/ChattORE.kt | 5 +- chattore/src/main/kotlin/Messenger.kt | 10 +- chattore/src/main/kotlin/feature/Bubble.kt | 265 +++++++++------------ 3 files changed, 120 insertions(+), 160 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 9493280..bd2ef5c 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -46,14 +46,15 @@ class ChattORE @Inject constructor( } pluginScope.apply { val emojis = createEmojiFeature() - val messenger = createMessenger(emojis, database, luckPerms, config.format) val userCache = createUserCache(database.database) + val bubbleManager = createBubbleManagerFeature() + val messenger = createMessenger(emojis, database, luckPerms, config.format, bubbleManager) + createBubbleFeature(messenger, userCache, database, bubbleManager) createAliasFeature() createChatFeature( messenger, ChatConfirmationConfig(config.regexes), ) - createBubbleFeature(messenger, userCache, database) createChattoreFeature() createDiscordFeature(messenger, emojis, config.discord) createFunCommandsFeature() diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 8e60a51..aad52b9 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -22,11 +22,12 @@ fun PluginScope.createMessenger( database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, + bubbleManager: BubbleManager ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, bubbleManager) } class Messenger( @@ -36,6 +37,7 @@ class Messenger( private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, private val fileTypeMap: Map>, + private val bubbleManager: BubbleManager ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -46,8 +48,6 @@ class Messenger( formatReplacement("~~", "st"), buildEmojiReplacement(emojis), ) - - val bubbleManager = BubbleManager() private var excludedCache: Set = emptySet() private var cacheDirty: Boolean = true @@ -75,8 +75,6 @@ class Messenger( fun broadcastChatMessage(originServer: String, player: Player, message: String) { val userId = player.uniqueId - val userManager = luckPerms.userManager - val luckUser = userManager.getUser(userId) ?: return val name = database.getNickname(userId) ?: NickPreset(player.username) val sender = "Click for more'>" @@ -109,8 +107,6 @@ class Messenger( fun broadcastBubbleMessage(player: Player, message: String) { val userId = player.uniqueId - val userManager = luckPerms.userManager - val luckUser = userManager.getUser(userId) ?: return val name = database.getNickname(userId) ?: NickPreset(player.username) val sender = "Click for more'>" diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 059b7fd..6f8cfff 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -1,11 +1,14 @@ package org.openredstone.chattore.feature import co.aikar.commands.BaseCommand +import co.aikar.commands.InvalidCommandArgument import co.aikar.commands.annotation.CommandAlias import co.aikar.commands.annotation.CommandCompletion import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Flags import co.aikar.commands.annotation.Single import co.aikar.commands.annotation.Subcommand +import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component @@ -14,10 +17,35 @@ import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* import java.util.UUID -val seeGlobalChatEnabled = Setting("seeGlobalChat") +val seeGlobalChatEnabled: Setting = Setting("seeGlobalChat") -fun PluginScope.createBubbleFeature(messenger: Messenger, userCache: UserCache, database: Storage) { - registerCommands(BubbleCommand(messenger, proxy, userCache, database)) +fun createBubbleManagerFeature(): BubbleManager { + return BubbleManager() +} + +fun PluginScope.createBubbleFeature( + messenger: Messenger, + userCache: UserCache, + database: Storage, + bubbleManager: BubbleManager +) { + commandManager.apply { + commandContexts.registerContext(Bubble::class.java) { ctx -> + val sender = ctx.sender as? Player + ?: throw InvalidCommandArgument("This command can only be used by players!") + val bubble = bubbleManager.getBubbleByPlayer(sender) + ?: throw InvalidCommandArgument("You are not in a Chat-Bubble!") + if (ctx.hasFlag("bubble-owned") && !bubble.isOwner(sender.uniqueId)) { + val ownerName = proxy.getPlayer(bubble.owner) + .map { it.username } + .orElse(bubble.owner.toString()) + throw InvalidCommandArgument("You are not the owner of this Chat-Bubble! ($ownerName is.)") + } + bubble + } + } + + registerCommands(BubbleCommand(messenger, proxy, userCache, database, bubbleManager)) } @CommandAlias("bubble|bb") @@ -26,206 +54,147 @@ private class BubbleCommand( private val messenger: Messenger, private val proxy: ProxyServer, private var userCache: UserCache, - private var database: Storage + private var database: Storage, + private var bubbleManager: BubbleManager ) : BaseCommand() { - private val Player.seeGlobalChat: Boolean get() = database.getSetting(seeGlobalChatEnabled, uniqueId) ?: false - @Subcommand("create") @CommandAlias("blow") fun create(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble != null) throw ChattoreException("You are already in a Chat-Bubble!") - messenger.bubbleManager.createBubble(sender.uniqueId) + if (bubbleManager.getBubbleByPlayer(sender) != null) + throw ChattoreException("You are already in a Chat-Bubble!") + bubbleManager.createBubble(sender.uniqueId) messenger.setCacheDirty() sender.sendInfo("Successfully created Chat-Bubble!") } @Subcommand("invite") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun invite(sender: Player, @Single target: String) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - - if (bubble.isInvited(player.uniqueId)) { - sender.sendInfo("${player.username} is already invited!") - return - } else if (bubble.containsPlayer(player.uniqueId)) { - sender.sendInfo("${player.username} is already in the Chat-Bubble!") - return + fun invite(sender: Player, bubble: Bubble, @Single target: OnlinePlayer) { + val player: Player = target.player + when { + bubble.isInvited(player.uniqueId) + -> sender.sendInfo("${player.username} is already invited!") + + bubble.containsPlayer(player.uniqueId) + -> sender.sendInfo("${player.username} is already in the Chat-Bubble!") + + else -> { + bubble.invitePlayer(player.uniqueId) + sender.sendInfo("Successfully invited ${player.username}!") + player.sendInfo("${sender.username} invited you to their Chat-Bubble!") + } } - - bubble.invitePlayer(player.uniqueId) - sender.sendInfo("Successfully invited ${player.username}!") - player.sendInfo("${sender.username} invited you to their Chat-Bubble!") - } @Subcommand("join") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun join(sender: Player, @Single target: String) { - val senderBubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (senderBubble != null) { - sender.sendInfo("You are already in a Chat-Bubble!") - return - } - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(player) + fun join(sender: Player, @Single target: OnlinePlayer) { + if (bubbleManager.getBubbleByPlayer(sender) != null) + throw ChattoreException("You are already in a Chat-Bubble!") + + val player: Player = target.player + val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") - - if (!bubble.isInvited(player.uniqueId) && bubble.isPrivate) { - sender.sendInfo("You were not invited to the Chat-Bubble!") - return - } + + if (!bubble.isInvited(sender.uniqueId) && bubble.isPrivate) + throw ChattoreException("You were not invited to the Chat-Bubble!") bubble.addPlayer(sender.uniqueId) messenger.setCacheDirty() bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("You successfully joined ${player.username}'s Chat-Bubble!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("You successfully joined ${player.username}'s Chat-Bubble!") } else { - target.sendInfo("${sender.username} joined your Chat-Bubble!") + member.sendInfo("${sender.username} joined your Chat-Bubble!") } } } @Subcommand("leave") - fun leave(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + fun leave(sender: Player, bubble: Bubble) { + val bubbleId = bubbleManager.getBubbleId(sender.uniqueId)!! bubble.removePlayer(sender.uniqueId) messenger.setCacheDirty() sender.sendInfo("You successfully left the Chat-Bubble!") if (bubble.players.isEmpty()) { - messenger.bubbleManager.removeBubble(bubbleId) + bubbleManager.removeBubble(bubbleId) return } bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - target.sendInfo("${sender.username} left your Chat-Bubble!") + proxy.getPlayer(uuid).orElse(null)?.sendInfo("${sender.username} left your Chat-Bubble!") } } @Subcommand("delete") @CommandAlias("pop") - fun delete(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - - if (!bubble.isOwner(sender.uniqueId)) { - val ownerName = proxy.getPlayer(bubble.owner) - sender.sendInfo("You are not the owner of this Chat-Bubble! (${ownerName} is.)") - return - } - val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + fun delete(sender: Player, @Flags("bubble-owned") bubble: Bubble) { + val bubbleId = bubbleManager.getBubbleId(sender.uniqueId)!! bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("You popped the Chat-Bubble!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("You popped the Chat-Bubble!") } else { - target.sendInfo("${sender.username} popped your Chat-Bubble!") + member.sendInfo("${sender.username} popped your Chat-Bubble!") } } - messenger.bubbleManager.removeBubble(bubbleId) + bubbleManager.removeBubble(bubbleId) messenger.setCacheDirty() } @Subcommand("kick") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun kick(sender: Player, @Single target: String) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - - if (player.uniqueId == sender.uniqueId) { - sender.sendInfo("You cannot kick yourself!") - return - } else if (!bubble.isOwner(sender.uniqueId)) { - val ownerName = proxy.getPlayer(bubble.owner) - sender.sendInfo("You are not the owner of this Chat-Bubble! (${ownerName} is.)") - return - } + fun kick(sender: Player, @Flags("bubble-owned") bubble: Bubble, @Single target: OnlinePlayer) { + val player: Player = target.player + if (player.uniqueId == sender.uniqueId) + throw ChattoreException("You cannot kick yourself!") bubble.removePlayer(player.uniqueId) messenger.setCacheDirty() player.sendInfo("You were kicked out of the Chat-Bubble!") bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("You kicked ${player.username} from the Chat-Bubble!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("You kicked ${player.username} from the Chat-Bubble!") } else { - target.sendInfo("${sender.username} kicked ${player.username} from your Chat-Bubble!") + member.sendInfo("${sender.username} kicked ${player.username} from your Chat-Bubble!") } } } @Subcommand("setPrivate") @CommandCompletion("true|false") - fun setPrivate(sender: Player, boolean: Boolean) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - if (!bubble.isOwner(sender.uniqueId)) { - val ownerName = proxy.getPlayer(bubble.owner) - sender.sendInfo("You are not the creator of this Chat-Bubble! (${ownerName} is.)") - return - } - + fun setPrivate(sender: Player, @Flags("bubble-owned") bubble: Bubble, boolean: Boolean) { bubble.isPrivate = boolean - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("The Chat-Bubble is now ${if (boolean) "private" else "public"}!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("The Chat-Bubble is now ${if (boolean) "private" else "public"}!") } else { - target.sendInfo("${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!") + member.sendInfo("${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!") } } } @Subcommand("list") fun list(sender: Player) { - if (messenger.bubbleManager.getBubbles().isEmpty()) { + if (bubbleManager.getBubbles().isEmpty()) { sender.sendInfo("There are currently no Chat-Bubbles!") return } - sender.sendRichMessage("Bubbles:") - - for (bubble in messenger.bubbleManager.getBubbles().values) { + for (bubble in bubbleManager.getBubbles().values) { val playersString = bubble.players .mapNotNull { uuid -> proxy.getPlayer(uuid).orElse(null)?.username } .joinToString(", ") - - val firstPlayer = bubble.players.firstOrNull()?.let { uuid -> - proxy.getPlayer(uuid).orElse(null)?.username - } - - val string = "$playersString | [Join]" - - val component = Component.text(string) - .clickEvent( - ClickEvent.suggestCommand("/bubble join $firstPlayer") - ) - .hoverEvent( - HoverEvent.showText(Component.text("Click to join bubble")) - ) - sender.sendMessage(component) + val firstPlayer = bubble.players.firstOrNull() + ?.let { proxy.getPlayer(it).orElse(null)?.username } + sender.sendMessage( + Component.text("$playersString | [Join]") + .clickEvent(ClickEvent.suggestCommand("/bubble join $firstPlayer")) + .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) + ) } } @@ -233,21 +202,16 @@ private class BubbleCommand( @CommandAlias("burst") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") @CommandPermission("chattore.bubble.manage") - fun forcedelete(sender: Player, @Single target: String) { - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(player) - if (bubble == null) { - sender.sendInfo("Target is not in a Chat-Bubble!") - return + fun forcedelete(sender: Player, @Single target: OnlinePlayer) { + val player: Player = target.player + val bubble = bubbleManager.getBubbleByPlayer(player) + ?: throw ChattoreException("Target is not in a Chat-Bubble!") + bubble.players.forEach { uuid -> + proxy.getPlayer(uuid).orElse(null)?.sendInfo("The Chat-Bubble was burst by ${sender.username}!") } - - val bubbleId = messenger.bubbleManager.getBubbleId(player.uniqueId)!! - messenger.bubbleManager.removeBubble(bubbleId) + val bubbleId = bubbleManager.getBubbleId(player.uniqueId)!! + bubbleManager.removeBubble(bubbleId) messenger.setCacheDirty() - sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") } @@ -261,18 +225,17 @@ private class BubbleCommand( fun seeGlobalChat(sender: Player, boolean: Boolean) { database.setSetting(seeGlobalChatEnabled, sender.uniqueId, boolean) messenger.setCacheDirty() - if (boolean) { - sender.sendInfo("You will now see global chat inside your Chat-Bubble!") - return - } - sender.sendInfo("You won't see global chat inside your Chat-Bubble anymore!") + sender.sendInfo( + if (boolean) "You will now see global chat inside your Chat-Bubble!" + else "You won't see global chat inside your Chat-Bubble anymore!" + ) } } data class Bubble( val owner: UUID, val players: MutableSet, - val invitedPlayers: MutableSet, + private val invitedPlayers: MutableSet, var isPrivate: Boolean ) { fun addPlayer(uuid: UUID): Boolean { @@ -306,8 +269,8 @@ data class Bubble( } class BubbleManager { - val bubbles: MutableMap = mutableMapOf() - var id: Int = 0 + private val bubbles: MutableMap = mutableMapOf() + private var id: Int = 0 fun getBubbles(): Map { return bubbles @@ -323,7 +286,7 @@ class BubbleManager { } fun getBubbleId(player: UUID): Int? { - return bubbles.entries.firstOrNull{it.value.players.contains(player)}?.key + return bubbles.entries.firstOrNull { it.value.players.contains(player) }?.key } fun getBubble(id: Int): Bubble? { @@ -331,6 +294,6 @@ class BubbleManager { } fun getBubbleByPlayer(player: Player): Bubble? { - return bubbles.entries.firstOrNull{it.value.players.contains(player.uniqueId)}?.value + return bubbles.entries.firstOrNull { it.value.players.contains(player.uniqueId) }?.value } -} \ No newline at end of file +} From 1b3effb560563c3d4c9254cc81a9f1bfcd605e55 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 27 Apr 2026 19:18:53 +0200 Subject: [PATCH 05/63] Changes! --- chattore/src/main/kotlin/ChattORE.kt | 6 +- chattore/src/main/kotlin/Messenger.kt | 10 +- chattore/src/main/kotlin/feature/Bubble.kt | 265 +++++++++------------ chattore/src/main/kotlin/feature/Chat.kt | 6 +- 4 files changed, 125 insertions(+), 162 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 9493280..b940f76 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -46,14 +46,16 @@ class ChattORE @Inject constructor( } pluginScope.apply { val emojis = createEmojiFeature() - val messenger = createMessenger(emojis, database, luckPerms, config.format) val userCache = createUserCache(database.database) + val bubbleManager = createBubbleManagerFeature() + val messenger = createMessenger(emojis, database, luckPerms, config.format, bubbleManager) + createBubbleFeature(messenger, userCache, database, bubbleManager) createAliasFeature() createChatFeature( messenger, ChatConfirmationConfig(config.regexes), + bubbleManager ) - createBubbleFeature(messenger, userCache, database) createChattoreFeature() createDiscordFeature(messenger, emojis, config.discord) createFunCommandsFeature() diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 8e60a51..aad52b9 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -22,11 +22,12 @@ fun PluginScope.createMessenger( database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, + bubbleManager: BubbleManager ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, bubbleManager) } class Messenger( @@ -36,6 +37,7 @@ class Messenger( private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, private val fileTypeMap: Map>, + private val bubbleManager: BubbleManager ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -46,8 +48,6 @@ class Messenger( formatReplacement("~~", "st"), buildEmojiReplacement(emojis), ) - - val bubbleManager = BubbleManager() private var excludedCache: Set = emptySet() private var cacheDirty: Boolean = true @@ -75,8 +75,6 @@ class Messenger( fun broadcastChatMessage(originServer: String, player: Player, message: String) { val userId = player.uniqueId - val userManager = luckPerms.userManager - val luckUser = userManager.getUser(userId) ?: return val name = database.getNickname(userId) ?: NickPreset(player.username) val sender = "Click for more'>" @@ -109,8 +107,6 @@ class Messenger( fun broadcastBubbleMessage(player: Player, message: String) { val userId = player.uniqueId - val userManager = luckPerms.userManager - val luckUser = userManager.getUser(userId) ?: return val name = database.getNickname(userId) ?: NickPreset(player.username) val sender = "Click for more'>" diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 059b7fd..6f8cfff 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -1,11 +1,14 @@ package org.openredstone.chattore.feature import co.aikar.commands.BaseCommand +import co.aikar.commands.InvalidCommandArgument import co.aikar.commands.annotation.CommandAlias import co.aikar.commands.annotation.CommandCompletion import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Flags import co.aikar.commands.annotation.Single import co.aikar.commands.annotation.Subcommand +import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component @@ -14,10 +17,35 @@ import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* import java.util.UUID -val seeGlobalChatEnabled = Setting("seeGlobalChat") +val seeGlobalChatEnabled: Setting = Setting("seeGlobalChat") -fun PluginScope.createBubbleFeature(messenger: Messenger, userCache: UserCache, database: Storage) { - registerCommands(BubbleCommand(messenger, proxy, userCache, database)) +fun createBubbleManagerFeature(): BubbleManager { + return BubbleManager() +} + +fun PluginScope.createBubbleFeature( + messenger: Messenger, + userCache: UserCache, + database: Storage, + bubbleManager: BubbleManager +) { + commandManager.apply { + commandContexts.registerContext(Bubble::class.java) { ctx -> + val sender = ctx.sender as? Player + ?: throw InvalidCommandArgument("This command can only be used by players!") + val bubble = bubbleManager.getBubbleByPlayer(sender) + ?: throw InvalidCommandArgument("You are not in a Chat-Bubble!") + if (ctx.hasFlag("bubble-owned") && !bubble.isOwner(sender.uniqueId)) { + val ownerName = proxy.getPlayer(bubble.owner) + .map { it.username } + .orElse(bubble.owner.toString()) + throw InvalidCommandArgument("You are not the owner of this Chat-Bubble! ($ownerName is.)") + } + bubble + } + } + + registerCommands(BubbleCommand(messenger, proxy, userCache, database, bubbleManager)) } @CommandAlias("bubble|bb") @@ -26,206 +54,147 @@ private class BubbleCommand( private val messenger: Messenger, private val proxy: ProxyServer, private var userCache: UserCache, - private var database: Storage + private var database: Storage, + private var bubbleManager: BubbleManager ) : BaseCommand() { - private val Player.seeGlobalChat: Boolean get() = database.getSetting(seeGlobalChatEnabled, uniqueId) ?: false - @Subcommand("create") @CommandAlias("blow") fun create(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (bubble != null) throw ChattoreException("You are already in a Chat-Bubble!") - messenger.bubbleManager.createBubble(sender.uniqueId) + if (bubbleManager.getBubbleByPlayer(sender) != null) + throw ChattoreException("You are already in a Chat-Bubble!") + bubbleManager.createBubble(sender.uniqueId) messenger.setCacheDirty() sender.sendInfo("Successfully created Chat-Bubble!") } @Subcommand("invite") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun invite(sender: Player, @Single target: String) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - - if (bubble.isInvited(player.uniqueId)) { - sender.sendInfo("${player.username} is already invited!") - return - } else if (bubble.containsPlayer(player.uniqueId)) { - sender.sendInfo("${player.username} is already in the Chat-Bubble!") - return + fun invite(sender: Player, bubble: Bubble, @Single target: OnlinePlayer) { + val player: Player = target.player + when { + bubble.isInvited(player.uniqueId) + -> sender.sendInfo("${player.username} is already invited!") + + bubble.containsPlayer(player.uniqueId) + -> sender.sendInfo("${player.username} is already in the Chat-Bubble!") + + else -> { + bubble.invitePlayer(player.uniqueId) + sender.sendInfo("Successfully invited ${player.username}!") + player.sendInfo("${sender.username} invited you to their Chat-Bubble!") + } } - - bubble.invitePlayer(player.uniqueId) - sender.sendInfo("Successfully invited ${player.username}!") - player.sendInfo("${sender.username} invited you to their Chat-Bubble!") - } @Subcommand("join") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun join(sender: Player, @Single target: String) { - val senderBubble = messenger.bubbleManager.getBubbleByPlayer(sender) - if (senderBubble != null) { - sender.sendInfo("You are already in a Chat-Bubble!") - return - } - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(player) + fun join(sender: Player, @Single target: OnlinePlayer) { + if (bubbleManager.getBubbleByPlayer(sender) != null) + throw ChattoreException("You are already in a Chat-Bubble!") + + val player: Player = target.player + val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") - - if (!bubble.isInvited(player.uniqueId) && bubble.isPrivate) { - sender.sendInfo("You were not invited to the Chat-Bubble!") - return - } + + if (!bubble.isInvited(sender.uniqueId) && bubble.isPrivate) + throw ChattoreException("You were not invited to the Chat-Bubble!") bubble.addPlayer(sender.uniqueId) messenger.setCacheDirty() bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("You successfully joined ${player.username}'s Chat-Bubble!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("You successfully joined ${player.username}'s Chat-Bubble!") } else { - target.sendInfo("${sender.username} joined your Chat-Bubble!") + member.sendInfo("${sender.username} joined your Chat-Bubble!") } } } @Subcommand("leave") - fun leave(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + fun leave(sender: Player, bubble: Bubble) { + val bubbleId = bubbleManager.getBubbleId(sender.uniqueId)!! bubble.removePlayer(sender.uniqueId) messenger.setCacheDirty() sender.sendInfo("You successfully left the Chat-Bubble!") if (bubble.players.isEmpty()) { - messenger.bubbleManager.removeBubble(bubbleId) + bubbleManager.removeBubble(bubbleId) return } bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - target.sendInfo("${sender.username} left your Chat-Bubble!") + proxy.getPlayer(uuid).orElse(null)?.sendInfo("${sender.username} left your Chat-Bubble!") } } @Subcommand("delete") @CommandAlias("pop") - fun delete(sender: Player) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - - if (!bubble.isOwner(sender.uniqueId)) { - val ownerName = proxy.getPlayer(bubble.owner) - sender.sendInfo("You are not the owner of this Chat-Bubble! (${ownerName} is.)") - return - } - val bubbleId = messenger.bubbleManager.getBubbleId(sender.uniqueId)!! + fun delete(sender: Player, @Flags("bubble-owned") bubble: Bubble) { + val bubbleId = bubbleManager.getBubbleId(sender.uniqueId)!! bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("You popped the Chat-Bubble!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("You popped the Chat-Bubble!") } else { - target.sendInfo("${sender.username} popped your Chat-Bubble!") + member.sendInfo("${sender.username} popped your Chat-Bubble!") } } - messenger.bubbleManager.removeBubble(bubbleId) + bubbleManager.removeBubble(bubbleId) messenger.setCacheDirty() } @Subcommand("kick") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun kick(sender: Player, @Single target: String) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - - if (player.uniqueId == sender.uniqueId) { - sender.sendInfo("You cannot kick yourself!") - return - } else if (!bubble.isOwner(sender.uniqueId)) { - val ownerName = proxy.getPlayer(bubble.owner) - sender.sendInfo("You are not the owner of this Chat-Bubble! (${ownerName} is.)") - return - } + fun kick(sender: Player, @Flags("bubble-owned") bubble: Bubble, @Single target: OnlinePlayer) { + val player: Player = target.player + if (player.uniqueId == sender.uniqueId) + throw ChattoreException("You cannot kick yourself!") bubble.removePlayer(player.uniqueId) messenger.setCacheDirty() player.sendInfo("You were kicked out of the Chat-Bubble!") bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("You kicked ${player.username} from the Chat-Bubble!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("You kicked ${player.username} from the Chat-Bubble!") } else { - target.sendInfo("${sender.username} kicked ${player.username} from your Chat-Bubble!") + member.sendInfo("${sender.username} kicked ${player.username} from your Chat-Bubble!") } } } @Subcommand("setPrivate") @CommandCompletion("true|false") - fun setPrivate(sender: Player, boolean: Boolean) { - val bubble = messenger.bubbleManager.getBubbleByPlayer(sender) - ?: throw ChattoreException("You are not in a Chat-Bubble!") - if (!bubble.isOwner(sender.uniqueId)) { - val ownerName = proxy.getPlayer(bubble.owner) - sender.sendInfo("You are not the creator of this Chat-Bubble! (${ownerName} is.)") - return - } - + fun setPrivate(sender: Player, @Flags("bubble-owned") bubble: Bubble, boolean: Boolean) { bubble.isPrivate = boolean - bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - if (target == sender) { - target.sendInfo("The Chat-Bubble is now ${if (boolean) "private" else "public"}!") + val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach + if (member == sender) { + member.sendInfo("The Chat-Bubble is now ${if (boolean) "private" else "public"}!") } else { - target.sendInfo("${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!") + member.sendInfo("${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!") } } } @Subcommand("list") fun list(sender: Player) { - if (messenger.bubbleManager.getBubbles().isEmpty()) { + if (bubbleManager.getBubbles().isEmpty()) { sender.sendInfo("There are currently no Chat-Bubbles!") return } - sender.sendRichMessage("Bubbles:") - - for (bubble in messenger.bubbleManager.getBubbles().values) { + for (bubble in bubbleManager.getBubbles().values) { val playersString = bubble.players .mapNotNull { uuid -> proxy.getPlayer(uuid).orElse(null)?.username } .joinToString(", ") - - val firstPlayer = bubble.players.firstOrNull()?.let { uuid -> - proxy.getPlayer(uuid).orElse(null)?.username - } - - val string = "$playersString | [Join]" - - val component = Component.text(string) - .clickEvent( - ClickEvent.suggestCommand("/bubble join $firstPlayer") - ) - .hoverEvent( - HoverEvent.showText(Component.text("Click to join bubble")) - ) - sender.sendMessage(component) + val firstPlayer = bubble.players.firstOrNull() + ?.let { proxy.getPlayer(it).orElse(null)?.username } + sender.sendMessage( + Component.text("$playersString | [Join]") + .clickEvent(ClickEvent.suggestCommand("/bubble join $firstPlayer")) + .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) + ) } } @@ -233,21 +202,16 @@ private class BubbleCommand( @CommandAlias("burst") @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") @CommandPermission("chattore.bubble.manage") - fun forcedelete(sender: Player, @Single target: String) { - val targetUuid = userCache.uuidOrNull(target) - ?: throw ChattoreException("We do not recognize that user!") - val player: Player = proxy.getPlayer(targetUuid) - .orElseThrow { ChattoreException("User is not online!") } - val bubble = messenger.bubbleManager.getBubbleByPlayer(player) - if (bubble == null) { - sender.sendInfo("Target is not in a Chat-Bubble!") - return + fun forcedelete(sender: Player, @Single target: OnlinePlayer) { + val player: Player = target.player + val bubble = bubbleManager.getBubbleByPlayer(player) + ?: throw ChattoreException("Target is not in a Chat-Bubble!") + bubble.players.forEach { uuid -> + proxy.getPlayer(uuid).orElse(null)?.sendInfo("The Chat-Bubble was burst by ${sender.username}!") } - - val bubbleId = messenger.bubbleManager.getBubbleId(player.uniqueId)!! - messenger.bubbleManager.removeBubble(bubbleId) + val bubbleId = bubbleManager.getBubbleId(player.uniqueId)!! + bubbleManager.removeBubble(bubbleId) messenger.setCacheDirty() - sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") } @@ -261,18 +225,17 @@ private class BubbleCommand( fun seeGlobalChat(sender: Player, boolean: Boolean) { database.setSetting(seeGlobalChatEnabled, sender.uniqueId, boolean) messenger.setCacheDirty() - if (boolean) { - sender.sendInfo("You will now see global chat inside your Chat-Bubble!") - return - } - sender.sendInfo("You won't see global chat inside your Chat-Bubble anymore!") + sender.sendInfo( + if (boolean) "You will now see global chat inside your Chat-Bubble!" + else "You won't see global chat inside your Chat-Bubble anymore!" + ) } } data class Bubble( val owner: UUID, val players: MutableSet, - val invitedPlayers: MutableSet, + private val invitedPlayers: MutableSet, var isPrivate: Boolean ) { fun addPlayer(uuid: UUID): Boolean { @@ -306,8 +269,8 @@ data class Bubble( } class BubbleManager { - val bubbles: MutableMap = mutableMapOf() - var id: Int = 0 + private val bubbles: MutableMap = mutableMapOf() + private var id: Int = 0 fun getBubbles(): Map { return bubbles @@ -323,7 +286,7 @@ class BubbleManager { } fun getBubbleId(player: UUID): Int? { - return bubbles.entries.firstOrNull{it.value.players.contains(player)}?.key + return bubbles.entries.firstOrNull { it.value.players.contains(player) }?.key } fun getBubble(id: Int): Bubble? { @@ -331,6 +294,6 @@ class BubbleManager { } fun getBubbleByPlayer(player: Player): Bubble? { - return bubbles.entries.firstOrNull{it.value.players.contains(player.uniqueId)}?.value + return bubbles.entries.firstOrNull { it.value.players.contains(player.uniqueId) }?.value } -} \ No newline at end of file +} diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index b64bb60..dede33a 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -22,10 +22,11 @@ data class ChatConfirmationConfig( fun PluginScope.createChatFeature( messenger: Messenger, config: ChatConfirmationConfig, + bubbleManager: BubbleManager ) { val flaggedMessages = ConcurrentHashMap() registerCommands(ConfirmMessage(flaggedMessages, logger, messenger)) - registerListeners(ChatListener(config, flaggedMessages, logger, messenger)) + registerListeners(ChatListener(config, flaggedMessages, logger, messenger, bubbleManager)) } private class ChatListener( @@ -33,6 +34,7 @@ private class ChatListener( private val flaggedMessages: ConcurrentHashMap, private val logger: Logger, private val messenger: Messenger, + private val bubbleManager: BubbleManager ) { private val regexes = config.regexes.mapNotNull { pattern -> @@ -48,7 +50,7 @@ private class ChatListener( if (isFlagged(player, message)) return logger.info("${player.username} (${player.uniqueId}): $message") player.currentServer.ifPresent { server -> - if (messenger.bubbleManager.getBubbleByPlayer(player) != null) { + if (bubbleManager.getBubbleByPlayer(player) != null) { messenger.broadcastBubbleMessage(player, message) } else { messenger.broadcastChatMessage(server.serverInfo.name, player, message) From 250be248af27a61937638def1f45db4b0da8ec9c Mon Sep 17 00:00:00 2001 From: Wueffi Date: Sun, 3 May 2026 18:03:35 +0200 Subject: [PATCH 06/63] The review of the review --- chattore/src/main/kotlin/ChattORE.kt | 5 +- chattore/src/main/kotlin/Messenger.kt | 13 +- chattore/src/main/kotlin/feature/Bubble.kt | 149 +++++++++------------ chattore/src/main/kotlin/feature/Chat.kt | 5 +- 4 files changed, 76 insertions(+), 96 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index b940f76..a810d03 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -47,9 +47,8 @@ class ChattORE @Inject constructor( pluginScope.apply { val emojis = createEmojiFeature() val userCache = createUserCache(database.database) - val bubbleManager = createBubbleManagerFeature() - val messenger = createMessenger(emojis, database, luckPerms, config.format, bubbleManager) - createBubbleFeature(messenger, userCache, database, bubbleManager) + val messenger = createMessenger(emojis, database, luckPerms, config.format) + val bubbleManager = createBubbleFeature(messenger, database) createAliasFeature() createChatFeature( messenger, diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index aad52b9..e7874be 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -10,6 +10,7 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextReplacementConfig import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms +import org.openredstone.chattore.feature.Bubble import org.openredstone.chattore.feature.BubbleManager import org.openredstone.chattore.feature.DiscordBroadcastEvent import org.openredstone.chattore.feature.Emojis @@ -22,12 +23,11 @@ fun PluginScope.createMessenger( database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, - bubbleManager: BubbleManager ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, bubbleManager) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) } class Messenger( @@ -37,7 +37,6 @@ class Messenger( private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, private val fileTypeMap: Map>, - private val bubbleManager: BubbleManager ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -105,7 +104,7 @@ class Messenger( proxy.eventManager.fireAndForget(discordBroadcast) } - fun broadcastBubbleMessage(player: Player, message: String) { + fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { val userId = player.uniqueId val name = database.getNickname(userId) ?: NickPreset(player.username) val sender = @@ -114,9 +113,7 @@ class Messenger( val compoPrefix = getPrefixComponent(userId, player) - val bubble = bubbleManager.getBubbleByPlayer(player) - - bubble?.players?.forEach { uuid -> + bubble.players.forEach { uuid -> val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach target.sendRichMessage( @@ -177,7 +174,7 @@ class Messenger( } fun rebuildExcludedCache() { - val bubbleExcluded = bubbleManager.getBubbles().values.flatMap { it.players }.toMutableSet() + val bubbleExcluded = BubbleManager.getBubbles().flatMap { it.players }.toMutableSet() bubbleExcluded.removeAll(database.getAllGlobalChatEnabled()) excludedCache = bubbleExcluded diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 6f8cfff..f0d9210 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -9,6 +9,7 @@ import co.aikar.commands.annotation.Flags import co.aikar.commands.annotation.Single import co.aikar.commands.annotation.Subcommand import co.aikar.commands.velocity.contexts.OnlinePlayer +import com.velocitypowered.api.event.player.PlayerChatEvent import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component @@ -18,24 +19,20 @@ import org.openredstone.chattore.* import java.util.UUID val seeGlobalChatEnabled: Setting = Setting("seeGlobalChat") - -fun createBubbleManagerFeature(): BubbleManager { - return BubbleManager() -} +const val BUBBLE_OWNED: String = "bubble-owned" fun PluginScope.createBubbleFeature( messenger: Messenger, - userCache: UserCache, database: Storage, - bubbleManager: BubbleManager -) { +): BubbleManager { + val bubbleManager = BubbleManager() commandManager.apply { - commandContexts.registerContext(Bubble::class.java) { ctx -> + commandContexts.registerIssuerOnlyContext(Bubble::class.java) { ctx -> val sender = ctx.sender as? Player ?: throw InvalidCommandArgument("This command can only be used by players!") val bubble = bubbleManager.getBubbleByPlayer(sender) ?: throw InvalidCommandArgument("You are not in a Chat-Bubble!") - if (ctx.hasFlag("bubble-owned") && !bubble.isOwner(sender.uniqueId)) { + if (ctx.hasFlag(BUBBLE_OWNED) && !bubble.isOwner(sender.uniqueId)) { val ownerName = proxy.getPlayer(bubble.owner) .map { it.username } .orElse(bubble.owner.toString()) @@ -44,8 +41,8 @@ fun PluginScope.createBubbleFeature( bubble } } - - registerCommands(BubbleCommand(messenger, proxy, userCache, database, bubbleManager)) + registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager)) + return bubbleManager } @CommandAlias("bubble|bb") @@ -53,7 +50,6 @@ fun PluginScope.createBubbleFeature( private class BubbleCommand( private val messenger: Messenger, private val proxy: ProxyServer, - private var userCache: UserCache, private var database: Storage, private var bubbleManager: BubbleManager ) : BaseCommand() { @@ -69,7 +65,6 @@ private class BubbleCommand( } @Subcommand("invite") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") fun invite(sender: Player, bubble: Bubble, @Single target: OnlinePlayer) { val player: Player = target.player when { @@ -88,7 +83,6 @@ private class BubbleCommand( } @Subcommand("join") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") fun join(sender: Player, @Single target: OnlinePlayer) { if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a Chat-Bubble!") @@ -102,50 +96,44 @@ private class BubbleCommand( bubble.addPlayer(sender.uniqueId) messenger.setCacheDirty() - bubble.players.forEach { uuid -> - val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - if (member == sender) { - member.sendInfo("You successfully joined ${player.username}'s Chat-Bubble!") - } else { - member.sendInfo("${sender.username} joined your Chat-Bubble!") - } - } + bubble.sendInfos( + sender, + "You successfully joined ${player.username}'s Chat-Bubble!", + "${sender.username} joined your Chat-Bubble!", + ) } @Subcommand("leave") fun leave(sender: Player, bubble: Bubble) { - val bubbleId = bubbleManager.getBubbleId(sender.uniqueId)!! bubble.removePlayer(sender.uniqueId) messenger.setCacheDirty() sender.sendInfo("You successfully left the Chat-Bubble!") if (bubble.players.isEmpty()) { - bubbleManager.removeBubble(bubbleId) + bubbleManager.removeBubble(bubble) return } - bubble.players.forEach { uuid -> - proxy.getPlayer(uuid).orElse(null)?.sendInfo("${sender.username} left your Chat-Bubble!") + if (bubble.owner == sender.uniqueId) { + val newOwner = bubble.players.first() + bubble.owner = newOwner + proxy.playerOrNull(newOwner)?.sendInfo("You are now the owner of the Chat-Bubble!") } + bubble.broadcastInfo("${sender.username} left your Chat-Bubble!") } @Subcommand("delete") @CommandAlias("pop") - fun delete(sender: Player, @Flags("bubble-owned") bubble: Bubble) { - val bubbleId = bubbleManager.getBubbleId(sender.uniqueId)!! - bubble.players.forEach { uuid -> - val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - if (member == sender) { - member.sendInfo("You popped the Chat-Bubble!") - } else { - member.sendInfo("${sender.username} popped your Chat-Bubble!") - } - } - bubbleManager.removeBubble(bubbleId) + fun delete(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble) { + bubble.sendInfos( + sender, + "You popped the the Chat-Bubble!", + "${sender.username} popped your Chat-Bubble!", + ) + bubbleManager.removeBubble(bubble) messenger.setCacheDirty() } @Subcommand("kick") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") - fun kick(sender: Player, @Flags("bubble-owned") bubble: Bubble, @Single target: OnlinePlayer) { + fun kick(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, @Single target: OnlinePlayer) { val player: Player = target.player if (player.uniqueId == sender.uniqueId) throw ChattoreException("You cannot kick yourself!") @@ -153,38 +141,32 @@ private class BubbleCommand( bubble.removePlayer(player.uniqueId) messenger.setCacheDirty() player.sendInfo("You were kicked out of the Chat-Bubble!") - bubble.players.forEach { uuid -> - val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - if (member == sender) { - member.sendInfo("You kicked ${player.username} from the Chat-Bubble!") - } else { - member.sendInfo("${sender.username} kicked ${player.username} from your Chat-Bubble!") - } - } + bubble.sendInfos( + sender, + "You kicked ${player.username} from the Chat-Bubble!", + "${sender.username} kicked ${player.username} from your Chat-Bubble!", + ) } @Subcommand("setPrivate") @CommandCompletion("true|false") - fun setPrivate(sender: Player, @Flags("bubble-owned") bubble: Bubble, boolean: Boolean) { + fun setPrivate(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, boolean: Boolean) { bubble.isPrivate = boolean - bubble.players.forEach { uuid -> - val member = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - if (member == sender) { - member.sendInfo("The Chat-Bubble is now ${if (boolean) "private" else "public"}!") - } else { - member.sendInfo("${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!") - } - } + bubble.sendInfos( + sender, + "The Chat-Bubble is now ${if (boolean) "private" else "public"}!", + "${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!", + ) } @Subcommand("list") fun list(sender: Player) { - if (bubbleManager.getBubbles().isEmpty()) { + if (BubbleManager.getBubbles().isEmpty()) { sender.sendInfo("There are currently no Chat-Bubbles!") return } sender.sendRichMessage("Bubbles:") - for (bubble in bubbleManager.getBubbles().values) { + for (bubble in BubbleManager.getBubbles()) { val playersString = bubble.players .mapNotNull { uuid -> proxy.getPlayer(uuid).orElse(null)?.username } .joinToString(", ") @@ -200,24 +182,20 @@ private class BubbleCommand( @Subcommand("forcedelete") @CommandAlias("burst") - @CommandCompletion("@${UserCache.COMPLETION_USERNAMES}") @CommandPermission("chattore.bubble.manage") fun forcedelete(sender: Player, @Single target: OnlinePlayer) { val player: Player = target.player val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") - bubble.players.forEach { uuid -> - proxy.getPlayer(uuid).orElse(null)?.sendInfo("The Chat-Bubble was burst by ${sender.username}!") - } - val bubbleId = bubbleManager.getBubbleId(player.uniqueId)!! - bubbleManager.removeBubble(bubbleId) + bubble.broadcastInfo("The Chat-Bubble was burst by ${sender.username}!") + bubbleManager.removeBubble(bubble) messenger.setCacheDirty() sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") } @CommandAlias("shout") fun shout(sender: Player, message: String) { - messenger.broadcastChatMessage(sender.currentServer.get().serverInfo.name, sender, message) + proxy.eventManager.fireAndForget(PlayerChatEvent(sender, message)) } @CommandAlias("seeglobalchat|gc") @@ -230,10 +208,23 @@ private class BubbleCommand( else "You won't see global chat inside your Chat-Bubble anymore!" ) } + + private fun Bubble.sendInfos(you: Player, yourMessage: String, theirMessage: String) { + players.forEach { uuid -> + val player = proxy.playerOrNull(uuid) ?: return@forEach + player.sendInfo(if (player == you) yourMessage else theirMessage) + } + } + + private fun Bubble.broadcastInfo(message: String) { + players.forEach { uuid -> + proxy.playerOrNull(uuid)?.sendInfo(message) + } + } } data class Bubble( - val owner: UUID, + var owner: UUID, val players: MutableSet, private val invitedPlayers: MutableSet, var isPrivate: Boolean @@ -269,31 +260,23 @@ data class Bubble( } class BubbleManager { - private val bubbles: MutableMap = mutableMapOf() - private var id: Int = 0 + companion object { + private val bubbles: MutableList = mutableListOf() - fun getBubbles(): Map { - return bubbles + fun getBubbles(): List { + return bubbles + } } fun createBubble(player: UUID) { - bubbles[id] = Bubble(player, mutableSetOf(player), mutableSetOf(), false) - id += 1 - } - - fun removeBubble(id: Int) { - bubbles.remove(id) - } - - fun getBubbleId(player: UUID): Int? { - return bubbles.entries.firstOrNull { it.value.players.contains(player) }?.key + bubbles.add(Bubble(player, mutableSetOf(player), mutableSetOf(), false)) } - fun getBubble(id: Int): Bubble? { - return bubbles[id] + fun removeBubble(bubble: Bubble) { + bubbles.remove(bubble) } fun getBubbleByPlayer(player: Player): Bubble? { - return bubbles.entries.firstOrNull { it.value.players.contains(player.uniqueId) }?.value + return bubbles.firstOrNull { it.players.contains(player.uniqueId) } } } diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index dede33a..7b30e21 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -50,8 +50,9 @@ private class ChatListener( if (isFlagged(player, message)) return logger.info("${player.username} (${player.uniqueId}): $message") player.currentServer.ifPresent { server -> - if (bubbleManager.getBubbleByPlayer(player) != null) { - messenger.broadcastBubbleMessage(player, message) + val bubble = bubbleManager.getBubbleByPlayer(player) + if (bubble != null) { + messenger.broadcastBubbleMessage(player, message, bubble) } else { messenger.broadcastChatMessage(server.serverInfo.name, player, message) } From 7a48000d0af3b2a10c80b879b29733b1e4e97fdd Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 4 May 2026 18:28:01 +0200 Subject: [PATCH 07/63] Another round of review --- chattore/src/main/kotlin/ChattORE.kt | 5 +- chattore/src/main/kotlin/ChattOREConfig.kt | 2 +- chattore/src/main/kotlin/Messenger.kt | 8 +- chattore/src/main/kotlin/Storage.kt | 6 +- chattore/src/main/kotlin/feature/Bubble.kt | 102 +++++++-------------- chattore/src/main/kotlin/feature/Chat.kt | 26 +++++- 6 files changed, 65 insertions(+), 84 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index a810d03..4d0fa68 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -47,8 +47,9 @@ class ChattORE @Inject constructor( pluginScope.apply { val emojis = createEmojiFeature() val userCache = createUserCache(database.database) - val messenger = createMessenger(emojis, database, luckPerms, config.format) - val bubbleManager = createBubbleFeature(messenger, database) + lateinit var bubbleManager: BubbleManager + val messenger = createMessenger(emojis, database, luckPerms, config.format) { bubbleManager } + bubbleManager = createBubbleFeature(messenger, database) createAliasFeature() createChatFeature( messenger, diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index 9ce5bbb..bdca445 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -18,7 +18,7 @@ data class FormatConfig( val global: String = " | : ", val join: String = " has joined the network", val leave: String = " has left the network", - val bubbleChatBubble: String = "[B] | : ", + val bubbleChat: String = "[B] | : ", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", ) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index e7874be..3a47b40 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -23,11 +23,12 @@ fun PluginScope.createMessenger( database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, + getBubbleManager: () -> BubbleManager, ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, getBubbleManager) } class Messenger( @@ -37,6 +38,7 @@ class Messenger( private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, private val fileTypeMap: Map>, + private val getBubbleManager: () -> BubbleManager, ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -117,7 +119,7 @@ class Messenger( val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach target.sendRichMessage( - formatConfig.bubbleChatBubble, + formatConfig.bubbleChat, "message" toC prepareChatMessage(message, player), "sender" toC sender, "prefix" toC compoPrefix, @@ -174,7 +176,7 @@ class Messenger( } fun rebuildExcludedCache() { - val bubbleExcluded = BubbleManager.getBubbles().flatMap { it.players }.toMutableSet() + val bubbleExcluded = getBubbleManager().getBubbles().flatMap { it.players }.toMutableSet() bubbleExcluded.removeAll(database.getAllGlobalChatEnabled()) excludedCache = bubbleExcluded diff --git a/chattore/src/main/kotlin/Storage.kt b/chattore/src/main/kotlin/Storage.kt index b7a47b1..c130613 100644 --- a/chattore/src/main/kotlin/Storage.kt +++ b/chattore/src/main/kotlin/Storage.kt @@ -6,7 +6,7 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import org.openredstone.chattore.feature.MailboxItem import org.openredstone.chattore.feature.NickPreset -import org.openredstone.chattore.feature.seeGlobalChatEnabled +import org.openredstone.chattore.feature.ShowGlobalChatInBubble import java.nio.file.Path import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -157,7 +157,7 @@ class Storage( fun getAllGlobalChatEnabled(): Set = transaction(database) { JsonSetting.selectAll().where { - (JsonSetting.key eq seeGlobalChatEnabled.key) and (JsonSetting.value eq Json.encodeToString(true)) - }.map { UUID.fromString(it[JsonSetting.uuid]) }.toSet() + (JsonSetting.key eq ShowGlobalChatInBubble.key) and (JsonSetting.value eq Json.encodeToString(true)) + }.map { UUID.fromString(it[JsonSetting.uuid]) }.toSet() } } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index f0d9210..b4aa26e 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -9,7 +9,6 @@ import co.aikar.commands.annotation.Flags import co.aikar.commands.annotation.Single import co.aikar.commands.annotation.Subcommand import co.aikar.commands.velocity.contexts.OnlinePlayer -import com.velocitypowered.api.event.player.PlayerChatEvent import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component @@ -18,8 +17,8 @@ import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* import java.util.UUID -val seeGlobalChatEnabled: Setting = Setting("seeGlobalChat") -const val BUBBLE_OWNED: String = "bubble-owned" +val ShowGlobalChatInBubble: Setting = Setting("showGlobalChatInBubble") +private const val BUBBLE_OWNED: String = "bubble-owned" fun PluginScope.createBubbleFeature( messenger: Messenger, @@ -32,7 +31,7 @@ fun PluginScope.createBubbleFeature( ?: throw InvalidCommandArgument("This command can only be used by players!") val bubble = bubbleManager.getBubbleByPlayer(sender) ?: throw InvalidCommandArgument("You are not in a Chat-Bubble!") - if (ctx.hasFlag(BUBBLE_OWNED) && !bubble.isOwner(sender.uniqueId)) { + if (ctx.hasFlag(BUBBLE_OWNED) && bubble.owner != sender.uniqueId) { val ownerName = proxy.getPlayer(bubble.owner) .map { it.username } .orElse(bubble.owner.toString()) @@ -67,19 +66,14 @@ private class BubbleCommand( @Subcommand("invite") fun invite(sender: Player, bubble: Bubble, @Single target: OnlinePlayer) { val player: Player = target.player - when { - bubble.isInvited(player.uniqueId) - -> sender.sendInfo("${player.username} is already invited!") + if (player.uniqueId in bubble.invitedPlayers) + throw ChattoreException("${player.username} is already invited!") + if (player.uniqueId in bubble.players) + throw ChattoreException("${player.username} is already in the Chat-Bubble!") - bubble.containsPlayer(player.uniqueId) - -> sender.sendInfo("${player.username} is already in the Chat-Bubble!") - - else -> { - bubble.invitePlayer(player.uniqueId) - sender.sendInfo("Successfully invited ${player.username}!") - player.sendInfo("${sender.username} invited you to their Chat-Bubble!") - } - } + bubble.invitedPlayers.add(player.uniqueId) + sender.sendInfo("Successfully invited ${player.username}!") + player.sendInfo("${sender.username} invited you to their Chat-Bubble!") } @Subcommand("join") @@ -91,10 +85,11 @@ private class BubbleCommand( val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") - if (!bubble.isInvited(sender.uniqueId) && bubble.isPrivate) + if (sender.uniqueId !in bubble.invitedPlayers && bubble.isPrivate) throw ChattoreException("You were not invited to the Chat-Bubble!") - bubble.addPlayer(sender.uniqueId) + bubble.invitedPlayers.remove(sender.uniqueId) + bubble.players.add(sender.uniqueId) messenger.setCacheDirty() bubble.sendInfos( sender, @@ -105,7 +100,7 @@ private class BubbleCommand( @Subcommand("leave") fun leave(sender: Player, bubble: Bubble) { - bubble.removePlayer(sender.uniqueId) + bubble.players.remove(sender.uniqueId) messenger.setCacheDirty() sender.sendInfo("You successfully left the Chat-Bubble!") if (bubble.players.isEmpty()) { @@ -138,7 +133,7 @@ private class BubbleCommand( if (player.uniqueId == sender.uniqueId) throw ChattoreException("You cannot kick yourself!") - bubble.removePlayer(player.uniqueId) + bubble.players.remove(player.uniqueId) messenger.setCacheDirty() player.sendInfo("You were kicked out of the Chat-Bubble!") bubble.sendInfos( @@ -150,23 +145,24 @@ private class BubbleCommand( @Subcommand("setPrivate") @CommandCompletion("true|false") - fun setPrivate(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, boolean: Boolean) { - bubble.isPrivate = boolean + fun setPrivate(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, isPrivate: Boolean) { + bubble.isPrivate = isPrivate + val visibility = if (isPrivate) "private" else "public" bubble.sendInfos( sender, - "The Chat-Bubble is now ${if (boolean) "private" else "public"}!", - "${sender.username} set the Chat-Bubble to ${if (boolean) "private" else "public"}!", + "The Chat-Bubble is now $visibility!", + "${sender.username} set the Chat-Bubble to $visibility!", ) } @Subcommand("list") fun list(sender: Player) { - if (BubbleManager.getBubbles().isEmpty()) { + if (bubbleManager.getBubbles().isEmpty()) { sender.sendInfo("There are currently no Chat-Bubbles!") return } sender.sendRichMessage("Bubbles:") - for (bubble in BubbleManager.getBubbles()) { + for (bubble in bubbleManager.getBubbles()) { val playersString = bubble.players .mapNotNull { uuid -> proxy.getPlayer(uuid).orElse(null)?.username } .joinToString(", ") @@ -193,15 +189,10 @@ private class BubbleCommand( sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") } - @CommandAlias("shout") - fun shout(sender: Player, message: String) { - proxy.eventManager.fireAndForget(PlayerChatEvent(sender, message)) - } - - @CommandAlias("seeglobalchat|gc") + @CommandAlias("showglobalchat|gc") @CommandCompletion("true|false") fun seeGlobalChat(sender: Player, boolean: Boolean) { - database.setSetting(seeGlobalChatEnabled, sender.uniqueId, boolean) + database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, boolean) messenger.setCacheDirty() sender.sendInfo( if (boolean) "You will now see global chat inside your Chat-Bubble!" @@ -223,49 +214,18 @@ private class BubbleCommand( } } -data class Bubble( +class Bubble( var owner: UUID, val players: MutableSet, - private val invitedPlayers: MutableSet, + val invitedPlayers: MutableSet, var isPrivate: Boolean -) { - fun addPlayer(uuid: UUID): Boolean { - if (!isPrivate) return players.add(uuid) - - if (uuid !in invitedPlayers) return false - - invitedPlayers.remove(uuid) - return players.add(uuid) - } - - fun removePlayer(uuid: UUID): Boolean { - return players.remove(uuid) - } - - fun invitePlayer(uuid: UUID): Boolean { - return invitedPlayers.add(uuid) - } - - fun containsPlayer(uuid: UUID): Boolean { - return players.contains(uuid) - } - - fun isInvited(uuid: UUID): Boolean { - return invitedPlayers.contains(uuid) - } - - fun isOwner(uuid: UUID): Boolean { - return owner == uuid - } -} +) class BubbleManager { - companion object { - private val bubbles: MutableList = mutableListOf() + private val bubbles: MutableList = mutableListOf() - fun getBubbles(): List { - return bubbles - } + fun getBubbles(): List { + return bubbles } fun createBubble(player: UUID) { @@ -277,6 +237,6 @@ class BubbleManager { } fun getBubbleByPlayer(player: Player): Bubble? { - return bubbles.firstOrNull { it.players.contains(player.uniqueId) } + return bubbles.firstOrNull { player.uniqueId in it.players } } } diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 7b30e21..dfbac5e 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -7,6 +7,7 @@ import co.aikar.commands.annotation.Default import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.PlayerChatEvent import com.velocitypowered.api.proxy.Player +import com.velocitypowered.api.proxy.ProxyServer import org.openredstone.chattore.ChattoreException import org.openredstone.chattore.Messenger import org.openredstone.chattore.PluginScope @@ -25,16 +26,19 @@ fun PluginScope.createChatFeature( bubbleManager: BubbleManager ) { val flaggedMessages = ConcurrentHashMap() + val shoutingPlayers = ConcurrentHashMap.newKeySet() registerCommands(ConfirmMessage(flaggedMessages, logger, messenger)) - registerListeners(ChatListener(config, flaggedMessages, logger, messenger, bubbleManager)) + registerCommands(Shout(proxy, shoutingPlayers)) + registerListeners(ChatListener(config, flaggedMessages, logger, messenger, bubbleManager, shoutingPlayers)) } private class ChatListener( - private val config: ChatConfirmationConfig, + config: ChatConfirmationConfig, private val flaggedMessages: ConcurrentHashMap, private val logger: Logger, private val messenger: Messenger, - private val bubbleManager: BubbleManager + private val bubbleManager: BubbleManager, + private val shoutingPlayers: MutableSet, ) { private val regexes = config.regexes.mapNotNull { pattern -> @@ -51,9 +55,10 @@ private class ChatListener( logger.info("${player.username} (${player.uniqueId}): $message") player.currentServer.ifPresent { server -> val bubble = bubbleManager.getBubbleByPlayer(player) - if (bubble != null) { + if (bubble != null && player.uniqueId !in shoutingPlayers) { messenger.broadcastBubbleMessage(player, message, bubble) } else { + shoutingPlayers.remove(player.uniqueId) messenger.broadcastChatMessage(server.serverInfo.name, player, message) } } @@ -97,3 +102,16 @@ private class ConfirmMessage( } } } + +@CommandAlias("shout") +@CommandPermission("chattore.bubble") +private class Shout( + private val proxy: ProxyServer, + private val shoutingPlayers: MutableSet, +) : BaseCommand() { + @Default + fun shout(sender: Player, message: String) { + shoutingPlayers.add(sender.uniqueId) + proxy.eventManager.fireAndForget(PlayerChatEvent(sender, message)) + } +} From 62418d2cf8c81ea5921624b1c01e687491cfc5da Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 3 May 2026 18:05:24 +0300 Subject: [PATCH 08/63] Fix handleCommandException `sender` is never Player, so the else branch was always taken. Now every error will also get the info prefix added to it. In the future, a sendError may be useful to add. --- chattore/src/main/kotlin/ChattORE.kt | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 4d0fa68..06434a5 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -1,16 +1,12 @@ package org.openredstone.chattore -import co.aikar.commands.BaseCommand -import co.aikar.commands.CommandIssuer -import co.aikar.commands.RegisteredCommand -import co.aikar.commands.VelocityCommandManager +import co.aikar.commands.* import com.google.inject.Inject import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyInitializeEvent import com.velocitypowered.api.plugin.Dependency import com.velocitypowered.api.plugin.Plugin import com.velocitypowered.api.plugin.annotation.DataDirectory -import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.luckperms.api.LuckPermsProvider import org.openredstone.chattore.feature.* @@ -94,11 +90,8 @@ class ChattORE @Inject constructor( ): Boolean { val exception = throwable as? ChattoreException ?: return false val message = exception.message ?: "Something went wrong!" - if (sender is Player) { - sender.sendSimpleS("Oh NO ! : ", message) - } else { - sender.sendMessage("Error: $message") - } + // cast ok because we're running on Velocity + (sender as VelocityCommandIssuer).issuer.sendInfoMM("", "message" toS message) return true } // TODO reloading functionality From 06c79bd6d3e44a7f2e07c7b709ab8b609cec278d Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Mon, 4 May 2026 23:15:23 +0300 Subject: [PATCH 09/63] Refactor sender and prefix construction into one method --- chattore/src/main/kotlin/Messenger.kt | 36 ++++++++------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 3a47b40..2c3ff05 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -10,13 +10,9 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextReplacementConfig import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms -import org.openredstone.chattore.feature.Bubble -import org.openredstone.chattore.feature.BubbleManager -import org.openredstone.chattore.feature.DiscordBroadcastEvent -import org.openredstone.chattore.feature.Emojis -import org.openredstone.chattore.feature.NickPreset +import org.openredstone.chattore.feature.* import java.net.URI -import java.util.UUID +import java.util.* fun PluginScope.createMessenger( emojis: Emojis, @@ -74,15 +70,21 @@ class Messenger( } .build() - fun broadcastChatMessage(originServer: String, player: Player, message: String) { + private fun senderAndPrefix(player: Player): Pair { val userId = player.uniqueId val name = database.getNickname(userId) ?: NickPreset(player.username) val sender = "Click for more'>" .renderSimpleC(name.render(player.username)) - val compoPrefix = getPrefixComponent(userId, player) + val luckUser = luckPerms.userManager.getUser(userId)!! // online users guaranteed to be loaded + val prefix = luckUser.cachedData.metaData.prefix + ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) + return sender to prefix.legacyDeserialize() + } + fun broadcastChatMessage(originServer: String, player: Player, message: String) { + val (sender, compoPrefix) = senderAndPrefix(player) if (cacheDirty) { rebuildExcludedCache() } @@ -107,14 +109,7 @@ class Messenger( } fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { - val userId = player.uniqueId - val name = database.getNickname(userId) ?: NickPreset(player.username) - val sender = - "Click for more'>" - .renderSimpleC(name.render(player.username)) - - val compoPrefix = getPrefixComponent(userId, player) - + val (sender, compoPrefix) = senderAndPrefix(player) bubble.players.forEach { uuid -> val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach @@ -187,15 +182,6 @@ class Messenger( cacheDirty = true } - private fun getPrefixComponent(userId: UUID, player: Player): Component { - val luckUser = luckPerms.userManager.getUser(userId) ?: return Component.empty() - - val prefix = luckUser.cachedData.metaData.prefix - ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) - - return prefix.legacyDeserialize() - } - private fun Component.performReplacements(replacements: List): Component = replacements.fold(this, Component::replaceText) } From 7cf002bbd6546615a0747e113cfecf0f822b6a39 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Mon, 4 May 2026 23:17:29 +0300 Subject: [PATCH 10/63] Small simplification --- chattore/src/main/kotlin/Messenger.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 2c3ff05..afeeba8 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -111,9 +111,7 @@ class Messenger( fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { val (sender, compoPrefix) = senderAndPrefix(player) bubble.players.forEach { uuid -> - val target = proxy.getPlayer(uuid).orElse(null) ?: return@forEach - - target.sendRichMessage( + proxy.playerOrNull(uuid)?.sendRichMessage( formatConfig.bubbleChat, "message" toC prepareChatMessage(message, player), "sender" toC sender, From 0caa0a41861657d8b30c87740e409f56ec4b3dbb Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 25 May 2026 16:46:23 +0200 Subject: [PATCH 11/63] Remove circular dependency as best as possible --- chattore/src/main/kotlin/ChattORE.kt | 5 ++-- chattore/src/main/kotlin/Messenger.kt | 27 ++++------------- chattore/src/main/kotlin/Storage.kt | 7 ----- chattore/src/main/kotlin/feature/Bubble.kt | 34 ++++++++++++++++------ 4 files changed, 32 insertions(+), 41 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 06434a5..e280a7e 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -43,9 +43,8 @@ class ChattORE @Inject constructor( pluginScope.apply { val emojis = createEmojiFeature() val userCache = createUserCache(database.database) - lateinit var bubbleManager: BubbleManager - val messenger = createMessenger(emojis, database, luckPerms, config.format) { bubbleManager } - bubbleManager = createBubbleFeature(messenger, database) + val messenger = createMessenger(emojis, database, luckPerms, config.format) + val bubbleManager = createBubbleFeature(messenger, database) createAliasFeature() createChatFeature( messenger, diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index afeeba8..8000417 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -13,18 +13,18 @@ import net.luckperms.api.LuckPerms import org.openredstone.chattore.feature.* import java.net.URI import java.util.* +import java.util.concurrent.ConcurrentHashMap fun PluginScope.createMessenger( emojis: Emojis, database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, - getBubbleManager: () -> BubbleManager, ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, getBubbleManager) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) } class Messenger( @@ -33,8 +33,7 @@ class Messenger( private val database: Storage, private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, - private val fileTypeMap: Map>, - private val getBubbleManager: () -> BubbleManager, + private val fileTypeMap: Map> ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -45,8 +44,7 @@ class Messenger( formatReplacement("~~", "st"), buildEmojiReplacement(emojis), ) - private var excludedCache: Set = emptySet() - private var cacheDirty: Boolean = true + val excludedFromGlobalChat: MutableSet = ConcurrentHashMap.newKeySet() private fun formatReplacement(key: String, tag: String): TextReplacementConfig = TextReplacementConfig.builder() @@ -85,11 +83,8 @@ class Messenger( fun broadcastChatMessage(originServer: String, player: Player, message: String) { val (sender, compoPrefix) = senderAndPrefix(player) - if (cacheDirty) { - rebuildExcludedCache() - } - proxy.allPlayers.filter { it.uniqueId !in excludedCache }.forEach { + proxy.allPlayers.filter { it.uniqueId !in excludedFromGlobalChat }.forEach { it.sendRichMessage( formatConfig.global, "message" toC prepareChatMessage(message, player), @@ -168,18 +163,6 @@ class Messenger( "").render() } - fun rebuildExcludedCache() { - val bubbleExcluded = getBubbleManager().getBubbles().flatMap { it.players }.toMutableSet() - bubbleExcluded.removeAll(database.getAllGlobalChatEnabled()) - - excludedCache = bubbleExcluded - cacheDirty = false - } - - fun setCacheDirty() { - cacheDirty = true - } - private fun Component.performReplacements(replacements: List): Component = replacements.fold(this, Component::replaceText) } diff --git a/chattore/src/main/kotlin/Storage.kt b/chattore/src/main/kotlin/Storage.kt index c130613..24c2b13 100644 --- a/chattore/src/main/kotlin/Storage.kt +++ b/chattore/src/main/kotlin/Storage.kt @@ -6,7 +6,6 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction import org.openredstone.chattore.feature.MailboxItem import org.openredstone.chattore.feature.NickPreset -import org.openredstone.chattore.feature.ShowGlobalChatInBubble import java.nio.file.Path import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -154,10 +153,4 @@ class Storage( val jsonString = result[JsonSetting.value] Json.decodeFromString(jsonString) } - - fun getAllGlobalChatEnabled(): Set = transaction(database) { - JsonSetting.selectAll().where { - (JsonSetting.key eq ShowGlobalChatInBubble.key) and (JsonSetting.value eq Json.encodeToString(true)) - }.map { UUID.fromString(it[JsonSetting.uuid]) }.toSet() - } } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index b4aa26e..4999a87 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -17,7 +17,7 @@ import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* import java.util.UUID -val ShowGlobalChatInBubble: Setting = Setting("showGlobalChatInBubble") +private val ShowGlobalChatInBubble: Setting = Setting("showGlobalChatInBubble") private const val BUBBLE_OWNED: String = "bubble-owned" fun PluginScope.createBubbleFeature( @@ -59,7 +59,7 @@ private class BubbleCommand( if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a Chat-Bubble!") bubbleManager.createBubble(sender.uniqueId) - messenger.setCacheDirty() + addExcluded(sender.uniqueId) sender.sendInfo("Successfully created Chat-Bubble!") } @@ -85,12 +85,12 @@ private class BubbleCommand( val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") - if (sender.uniqueId !in bubble.invitedPlayers && bubble.isPrivate) + if (bubble.isPrivate && sender.uniqueId !in bubble.invitedPlayers) throw ChattoreException("You were not invited to the Chat-Bubble!") bubble.invitedPlayers.remove(sender.uniqueId) bubble.players.add(sender.uniqueId) - messenger.setCacheDirty() + addExcluded(sender.uniqueId) bubble.sendInfos( sender, "You successfully joined ${player.username}'s Chat-Bubble!", @@ -101,7 +101,7 @@ private class BubbleCommand( @Subcommand("leave") fun leave(sender: Player, bubble: Bubble) { bubble.players.remove(sender.uniqueId) - messenger.setCacheDirty() + removeExcluded(sender.uniqueId) sender.sendInfo("You successfully left the Chat-Bubble!") if (bubble.players.isEmpty()) { bubbleManager.removeBubble(bubble) @@ -123,8 +123,8 @@ private class BubbleCommand( "You popped the the Chat-Bubble!", "${sender.username} popped your Chat-Bubble!", ) + messenger.excludedFromGlobalChat.removeAll(bubble.players) bubbleManager.removeBubble(bubble) - messenger.setCacheDirty() } @Subcommand("kick") @@ -134,7 +134,7 @@ private class BubbleCommand( throw ChattoreException("You cannot kick yourself!") bubble.players.remove(player.uniqueId) - messenger.setCacheDirty() + removeExcluded(player.uniqueId) player.sendInfo("You were kicked out of the Chat-Bubble!") bubble.sendInfos( sender, @@ -184,8 +184,8 @@ private class BubbleCommand( val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") bubble.broadcastInfo("The Chat-Bubble was burst by ${sender.username}!") + messenger.excludedFromGlobalChat.removeAll(bubble.players) bubbleManager.removeBubble(bubble) - messenger.setCacheDirty() sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") } @@ -193,7 +193,13 @@ private class BubbleCommand( @CommandCompletion("true|false") fun seeGlobalChat(sender: Player, boolean: Boolean) { database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, boolean) - messenger.setCacheDirty() + if (boolean) { + messenger.excludedFromGlobalChat.remove(sender.uniqueId) + } else { + if (bubbleManager.getBubbleByPlayer(sender) != null) { + messenger.excludedFromGlobalChat.add(sender.uniqueId) + } + } sender.sendInfo( if (boolean) "You will now see global chat inside your Chat-Bubble!" else "You won't see global chat inside your Chat-Bubble anymore!" @@ -212,6 +218,16 @@ private class BubbleCommand( proxy.playerOrNull(uuid)?.sendInfo(message) } } + + private fun addExcluded(uuid: UUID) { + if (database.getSetting(ShowGlobalChatInBubble, uuid) != true) { + messenger.excludedFromGlobalChat.add(uuid) + } + } + + private fun removeExcluded(uuid: UUID) { + messenger.excludedFromGlobalChat.remove(uuid) + } } class Bubble( From 74a76aee69e7fa9ac8ceab89f9ed4170eddc8059 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Mon, 25 May 2026 18:21:51 +0200 Subject: [PATCH 12/63] Make shouting go through chat confirmations and fix two other small things! --- chattore/src/main/kotlin/feature/Bubble.kt | 16 ++++++---------- chattore/src/main/kotlin/feature/Chat.kt | 22 ++++++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 4999a87..ee15275 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -101,7 +101,7 @@ private class BubbleCommand( @Subcommand("leave") fun leave(sender: Player, bubble: Bubble) { bubble.players.remove(sender.uniqueId) - removeExcluded(sender.uniqueId) + messenger.excludedFromGlobalChat.remove(sender.uniqueId) sender.sendInfo("You successfully left the Chat-Bubble!") if (bubble.players.isEmpty()) { bubbleManager.removeBubble(bubble) @@ -134,7 +134,7 @@ private class BubbleCommand( throw ChattoreException("You cannot kick yourself!") bubble.players.remove(player.uniqueId) - removeExcluded(player.uniqueId) + messenger.excludedFromGlobalChat.remove(player.uniqueId) player.sendInfo("You were kicked out of the Chat-Bubble!") bubble.sendInfos( sender, @@ -191,9 +191,9 @@ private class BubbleCommand( @CommandAlias("showglobalchat|gc") @CommandCompletion("true|false") - fun seeGlobalChat(sender: Player, boolean: Boolean) { - database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, boolean) - if (boolean) { + fun seeGlobalChat(sender: Player, showGlobalChat: Boolean) { + database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, showGlobalChat) + if (showGlobalChat) { messenger.excludedFromGlobalChat.remove(sender.uniqueId) } else { if (bubbleManager.getBubbleByPlayer(sender) != null) { @@ -201,7 +201,7 @@ private class BubbleCommand( } } sender.sendInfo( - if (boolean) "You will now see global chat inside your Chat-Bubble!" + if (showGlobalChat) "You will now see global chat inside your Chat-Bubble!" else "You won't see global chat inside your Chat-Bubble anymore!" ) } @@ -224,10 +224,6 @@ private class BubbleCommand( messenger.excludedFromGlobalChat.add(uuid) } } - - private fun removeExcluded(uuid: UUID) { - messenger.excludedFromGlobalChat.remove(uuid) - } } class Bubble( diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index dfbac5e..cf6a4d9 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -26,10 +26,9 @@ fun PluginScope.createChatFeature( bubbleManager: BubbleManager ) { val flaggedMessages = ConcurrentHashMap() - val shoutingPlayers = ConcurrentHashMap.newKeySet() registerCommands(ConfirmMessage(flaggedMessages, logger, messenger)) - registerCommands(Shout(proxy, shoutingPlayers)) - registerListeners(ChatListener(config, flaggedMessages, logger, messenger, bubbleManager, shoutingPlayers)) + val chatListener = ChatListener(config, flaggedMessages, logger, messenger, bubbleManager) + registerCommands(Shout(chatListener)) } private class ChatListener( @@ -37,8 +36,7 @@ private class ChatListener( private val flaggedMessages: ConcurrentHashMap, private val logger: Logger, private val messenger: Messenger, - private val bubbleManager: BubbleManager, - private val shoutingPlayers: MutableSet, + private val bubbleManager: BubbleManager ) { private val regexes = config.regexes.mapNotNull { pattern -> @@ -51,14 +49,16 @@ private class ChatListener( fun onChatEvent(event: PlayerChatEvent) { val player = event.player val message = event.message + handleChat(player, message, bubbleManager.getBubbleByPlayer(player)) + } + + fun handleChat(player: Player, message: String, bubble: Bubble?) { if (isFlagged(player, message)) return logger.info("${player.username} (${player.uniqueId}): $message") player.currentServer.ifPresent { server -> - val bubble = bubbleManager.getBubbleByPlayer(player) - if (bubble != null && player.uniqueId !in shoutingPlayers) { + if (bubble != null) { messenger.broadcastBubbleMessage(player, message, bubble) } else { - shoutingPlayers.remove(player.uniqueId) messenger.broadcastChatMessage(server.serverInfo.name, player, message) } } @@ -106,12 +106,10 @@ private class ConfirmMessage( @CommandAlias("shout") @CommandPermission("chattore.bubble") private class Shout( - private val proxy: ProxyServer, - private val shoutingPlayers: MutableSet, + private val chatListener: ChatListener, ) : BaseCommand() { @Default fun shout(sender: Player, message: String) { - shoutingPlayers.add(sender.uniqueId) - proxy.eventManager.fireAndForget(PlayerChatEvent(sender, message)) + chatListener.handleChat(sender, message, bubble = null) // passing bubble = null to shout } } From ea8e6d419f746c1bcad9cc69b109d01b0c60ae64 Mon Sep 17 00:00:00 2001 From: Wueffi Date: Wed, 27 May 2026 18:44:13 +0200 Subject: [PATCH 13/63] Reformat code! --- chattore/src/main/kotlin/Messenger.kt | 7 +++++-- chattore/src/main/kotlin/feature/Bubble.kt | 13 ++++--------- chattore/src/main/kotlin/feature/Chat.kt | 5 ++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 8000417..a7ca8bf 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -10,7 +10,10 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextReplacementConfig import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms -import org.openredstone.chattore.feature.* +import org.openredstone.chattore.feature.Bubble +import org.openredstone.chattore.feature.DiscordBroadcastEvent +import org.openredstone.chattore.feature.Emojis +import org.openredstone.chattore.feature.NickPreset import java.net.URI import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -33,7 +36,7 @@ class Messenger( private val database: Storage, private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, - private val fileTypeMap: Map> + private val fileTypeMap: Map>, ) { private val urlRegex = """]+)?)>?""".toRegex() diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index ee15275..3afb3e7 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -2,12 +2,7 @@ package org.openredstone.chattore.feature import co.aikar.commands.BaseCommand import co.aikar.commands.InvalidCommandArgument -import co.aikar.commands.annotation.CommandAlias -import co.aikar.commands.annotation.CommandCompletion -import co.aikar.commands.annotation.CommandPermission -import co.aikar.commands.annotation.Flags -import co.aikar.commands.annotation.Single -import co.aikar.commands.annotation.Subcommand +import co.aikar.commands.annotation.* import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer @@ -15,7 +10,7 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* -import java.util.UUID +import java.util.* private val ShowGlobalChatInBubble: Setting = Setting("showGlobalChatInBubble") private const val BUBBLE_OWNED: String = "bubble-owned" @@ -50,7 +45,7 @@ private class BubbleCommand( private val messenger: Messenger, private val proxy: ProxyServer, private var database: Storage, - private var bubbleManager: BubbleManager + private var bubbleManager: BubbleManager, ) : BaseCommand() { @Subcommand("create") @@ -230,7 +225,7 @@ class Bubble( var owner: UUID, val players: MutableSet, val invitedPlayers: MutableSet, - var isPrivate: Boolean + var isPrivate: Boolean, ) class BubbleManager { diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index cf6a4d9..dc67db1 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -7,7 +7,6 @@ import co.aikar.commands.annotation.Default import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.PlayerChatEvent import com.velocitypowered.api.proxy.Player -import com.velocitypowered.api.proxy.ProxyServer import org.openredstone.chattore.ChattoreException import org.openredstone.chattore.Messenger import org.openredstone.chattore.PluginScope @@ -23,7 +22,7 @@ data class ChatConfirmationConfig( fun PluginScope.createChatFeature( messenger: Messenger, config: ChatConfirmationConfig, - bubbleManager: BubbleManager + bubbleManager: BubbleManager, ) { val flaggedMessages = ConcurrentHashMap() registerCommands(ConfirmMessage(flaggedMessages, logger, messenger)) @@ -36,7 +35,7 @@ private class ChatListener( private val flaggedMessages: ConcurrentHashMap, private val logger: Logger, private val messenger: Messenger, - private val bubbleManager: BubbleManager + private val bubbleManager: BubbleManager, ) { private val regexes = config.regexes.mapNotNull { pattern -> From f56ca1aa794eced457e24ae19e3befb611712d7e Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 07:13:35 +0300 Subject: [PATCH 14/63] Update Gradle (8.14.1 -> 9.5.1) --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca5caab..a006b7f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jan 21 15:46:13 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From e082e7b52f121880816344ce2ef59f5553c9b5f3 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 07:31:04 +0300 Subject: [PATCH 15/63] Update Gradle shadow plugin (8.3.6 -> 9.4.2) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce4bc58..8a47d62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ jackson-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-modul jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } [plugins] -shadow = { id = "com.gradleup.shadow", version = "8.3.6" } +shadow = { id = "com.gradleup.shadow", version = "9.4.2" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } From 553d3f7df40dca4d32c43e92afa6e283ee16ca3c Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 07:31:21 +0300 Subject: [PATCH 16/63] Prevent test task from failing if no tests are discovered --- build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 251744d..8fdf68c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,3 +6,9 @@ plugins { alias(libs.plugins.pluginYml) apply false alias(libs.plugins.buildconfig) apply false } + +allprojects { + tasks.withType { + failOnNoDiscoveredTests = false + } +} From dc971a22dd37eb159539e4fa7dc03ad15b908dca Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:04:12 +0300 Subject: [PATCH 17/63] Add trailing comma --- chattore/src/main/kotlin/Messenger.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index a7ca8bf..7b6b0f9 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -92,7 +92,7 @@ class Messenger( formatConfig.global, "message" toC prepareChatMessage(message, player), "sender" toC sender, - "prefix" toC compoPrefix + "prefix" toC compoPrefix, ) } From c55949938ee91ed677a3d35730f6d77968e3f0ae Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:04:43 +0300 Subject: [PATCH 18/63] Add chat listener registration back --- chattore/src/main/kotlin/feature/Chat.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index dc67db1..ac05282 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -25,9 +25,9 @@ fun PluginScope.createChatFeature( bubbleManager: BubbleManager, ) { val flaggedMessages = ConcurrentHashMap() - registerCommands(ConfirmMessage(flaggedMessages, logger, messenger)) val chatListener = ChatListener(config, flaggedMessages, logger, messenger, bubbleManager) - registerCommands(Shout(chatListener)) + registerListeners(chatListener) + registerCommands(ConfirmMessage(flaggedMessages, logger, messenger), Shout(chatListener)) } private class ChatListener( From 7741c1640c5f6bf9b3eeb002ca4d5b0b7b68ce1b Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:07:15 +0300 Subject: [PATCH 19/63] Remove some type annotations and @Single annotations --- chattore/src/main/kotlin/feature/Bubble.kt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 3afb3e7..58fce74 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -12,8 +12,8 @@ import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* import java.util.* -private val ShowGlobalChatInBubble: Setting = Setting("showGlobalChatInBubble") -private const val BUBBLE_OWNED: String = "bubble-owned" +private val ShowGlobalChatInBubble = Setting("showGlobalChatInBubble") +private const val BUBBLE_OWNED = "bubble-owned" fun PluginScope.createBubbleFeature( messenger: Messenger, @@ -59,8 +59,8 @@ private class BubbleCommand( } @Subcommand("invite") - fun invite(sender: Player, bubble: Bubble, @Single target: OnlinePlayer) { - val player: Player = target.player + fun invite(sender: Player, bubble: Bubble, target: OnlinePlayer) { + val player = target.player if (player.uniqueId in bubble.invitedPlayers) throw ChattoreException("${player.username} is already invited!") if (player.uniqueId in bubble.players) @@ -72,11 +72,11 @@ private class BubbleCommand( } @Subcommand("join") - fun join(sender: Player, @Single target: OnlinePlayer) { + fun join(sender: Player, target: OnlinePlayer) { if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a Chat-Bubble!") - val player: Player = target.player + val player = target.player val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") @@ -123,8 +123,8 @@ private class BubbleCommand( } @Subcommand("kick") - fun kick(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, @Single target: OnlinePlayer) { - val player: Player = target.player + fun kick(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, target: OnlinePlayer) { + val player = target.player if (player.uniqueId == sender.uniqueId) throw ChattoreException("You cannot kick yourself!") @@ -174,8 +174,8 @@ private class BubbleCommand( @Subcommand("forcedelete") @CommandAlias("burst") @CommandPermission("chattore.bubble.manage") - fun forcedelete(sender: Player, @Single target: OnlinePlayer) { - val player: Player = target.player + fun forcedelete(sender: Player, target: OnlinePlayer) { + val player = target.player val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("Target is not in a Chat-Bubble!") bubble.broadcastInfo("The Chat-Bubble was burst by ${sender.username}!") From 261ca16dc24cf9c142be7222660412c648402f0d Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:08:22 +0300 Subject: [PATCH 20/63] Use playerOrNull, merge else if --- chattore/src/main/kotlin/feature/Bubble.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 58fce74..c7b0649 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -27,9 +27,7 @@ fun PluginScope.createBubbleFeature( val bubble = bubbleManager.getBubbleByPlayer(sender) ?: throw InvalidCommandArgument("You are not in a Chat-Bubble!") if (ctx.hasFlag(BUBBLE_OWNED) && bubble.owner != sender.uniqueId) { - val ownerName = proxy.getPlayer(bubble.owner) - .map { it.username } - .orElse(bubble.owner.toString()) + val ownerName = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() throw InvalidCommandArgument("You are not the owner of this Chat-Bubble! ($ownerName is.)") } bubble @@ -159,10 +157,10 @@ private class BubbleCommand( sender.sendRichMessage("Bubbles:") for (bubble in bubbleManager.getBubbles()) { val playersString = bubble.players - .mapNotNull { uuid -> proxy.getPlayer(uuid).orElse(null)?.username } + .mapNotNull { uuid -> proxy.playerOrNull(uuid)?.username } .joinToString(", ") val firstPlayer = bubble.players.firstOrNull() - ?.let { proxy.getPlayer(it).orElse(null)?.username } + ?.let { proxy.playerOrNull(it)?.username } sender.sendMessage( Component.text("$playersString | [Join]") .clickEvent(ClickEvent.suggestCommand("/bubble join $firstPlayer")) @@ -190,10 +188,8 @@ private class BubbleCommand( database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, showGlobalChat) if (showGlobalChat) { messenger.excludedFromGlobalChat.remove(sender.uniqueId) - } else { - if (bubbleManager.getBubbleByPlayer(sender) != null) { - messenger.excludedFromGlobalChat.add(sender.uniqueId) - } + } else if (bubbleManager.getBubbleByPlayer(sender) != null) { + messenger.excludedFromGlobalChat.add(sender.uniqueId) } sender.sendInfo( if (showGlobalChat) "You will now see global chat inside your Chat-Bubble!" From 061dc6a424eb486e8311e3ed7621b16865b0c81d Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:08:54 +0300 Subject: [PATCH 21/63] Convert getBubbles() to property instead --- chattore/src/main/kotlin/feature/Bubble.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index c7b0649..5bc2e85 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -150,12 +150,12 @@ private class BubbleCommand( @Subcommand("list") fun list(sender: Player) { - if (bubbleManager.getBubbles().isEmpty()) { + if (bubbleManager.bubbles.isEmpty()) { sender.sendInfo("There are currently no Chat-Bubbles!") return } sender.sendRichMessage("Bubbles:") - for (bubble in bubbleManager.getBubbles()) { + for (bubble in bubbleManager.bubbles) { val playersString = bubble.players .mapNotNull { uuid -> proxy.playerOrNull(uuid)?.username } .joinToString(", ") @@ -225,21 +225,18 @@ class Bubble( ) class BubbleManager { - private val bubbles: MutableList = mutableListOf() - - fun getBubbles(): List { - return bubbles - } + private val _bubbles: MutableList = mutableListOf() + val bubbles: List get() = _bubbles fun createBubble(player: UUID) { - bubbles.add(Bubble(player, mutableSetOf(player), mutableSetOf(), false)) + _bubbles.add(Bubble(player, mutableSetOf(player), mutableSetOf(), false)) } fun removeBubble(bubble: Bubble) { - bubbles.remove(bubble) + _bubbles.remove(bubble) } fun getBubbleByPlayer(player: Player): Bubble? { - return bubbles.firstOrNull { player.uniqueId in it.players } + return _bubbles.firstOrNull { player.uniqueId in it.players } } } From becb6fd7f243c1477f2b76cdd8432ccbe299bddd Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:18:58 +0300 Subject: [PATCH 22/63] Fix bubble list formatting --- chattore/src/main/kotlin/feature/Bubble.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 5bc2e85..66135d3 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -159,12 +159,16 @@ private class BubbleCommand( val playersString = bubble.players .mapNotNull { uuid -> proxy.playerOrNull(uuid)?.username } .joinToString(", ") - val firstPlayer = bubble.players.firstOrNull() - ?.let { proxy.playerOrNull(it)?.username } + val owner = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() + val joinButton = "[Join]".render() + .clickEvent(ClickEvent.suggestCommand("/bubble join $owner")) + .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) sender.sendMessage( - Component.text("$playersString | [Join]") - .clickEvent(ClickEvent.suggestCommand("/bubble join $firstPlayer")) - .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) + Component.textOfChildren( + Component.text(playersString), + " | ".render(), + joinButton, + ) ) } } From 899c06db279946d1d5a88c251784d8188ee1a758 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:44:56 +0300 Subject: [PATCH 23/63] Add hover with bubble info on bubble messages --- chattore/src/main/kotlin/ChattOREConfig.kt | 2 +- chattore/src/main/kotlin/Messenger.kt | 2 ++ chattore/src/main/kotlin/feature/Bubble.kt | 14 +++++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index bdca445..0e19486 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -18,7 +18,7 @@ data class FormatConfig( val global: String = " | : ", val join: String = " has joined the network", val leave: String = " has left the network", - val bubbleChat: String = "[B] | : ", + val bubbleChat: String = "[B] | : ", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", ) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 7b6b0f9..9ffe281 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextReplacementConfig +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms import org.openredstone.chattore.feature.Bubble @@ -114,6 +115,7 @@ class Messenger( "message" toC prepareChatMessage(message, player), "sender" toC sender, "prefix" toC compoPrefix, + Placeholder.styling("bubbleinfo", bubble.formatInfo(proxy)), ) } } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 66135d3..440fa02 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -156,16 +156,13 @@ private class BubbleCommand( } sender.sendRichMessage("Bubbles:") for (bubble in bubbleManager.bubbles) { - val playersString = bubble.players - .mapNotNull { uuid -> proxy.playerOrNull(uuid)?.username } - .joinToString(", ") val owner = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() val joinButton = "[Join]".render() .clickEvent(ClickEvent.suggestCommand("/bubble join $owner")) .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) sender.sendMessage( Component.textOfChildren( - Component.text(playersString), + Component.text(bubble.playersString(proxy)), " | ".render(), joinButton, ) @@ -226,7 +223,14 @@ class Bubble( val players: MutableSet, val invitedPlayers: MutableSet, var isPrivate: Boolean, -) +) { + fun playersString(proxy: ProxyServer) = players + .mapNotNull { uuid -> proxy.playerOrNull(uuid)?.username } + .joinToString(", ") + + fun formatInfo(proxy: ProxyServer) = + HoverEvent.showText(Component.text("In a bubble with: ${playersString(proxy)}")) +} class BubbleManager { private val _bubbles: MutableList = mutableListOf() From db1de3c530d3a4dfafe9b997a3c0aea7f58d5e9a Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 08:45:25 +0300 Subject: [PATCH 24/63] Convert to expression body --- chattore/src/main/kotlin/feature/Bubble.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 440fa02..afeb5b1 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -244,7 +244,5 @@ class BubbleManager { _bubbles.remove(bubble) } - fun getBubbleByPlayer(player: Player): Bubble? { - return _bubbles.firstOrNull { player.uniqueId in it.players } - } + fun getBubbleByPlayer(player: Player): Bubble? = _bubbles.firstOrNull { player.uniqueId in it.players } } From 474ef6032dbdddc607724c7aeb67c064004c4941 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 09:16:15 +0300 Subject: [PATCH 25/63] Tweak bubble messages a little --- chattore/src/main/kotlin/feature/Bubble.kt | 67 +++++++++++----------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index afeb5b1..661855e 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -25,10 +25,12 @@ fun PluginScope.createBubbleFeature( val sender = ctx.sender as? Player ?: throw InvalidCommandArgument("This command can only be used by players!") val bubble = bubbleManager.getBubbleByPlayer(sender) - ?: throw InvalidCommandArgument("You are not in a Chat-Bubble!") + ?: throw InvalidCommandArgument("You are not in a bubble!") if (ctx.hasFlag(BUBBLE_OWNED) && bubble.owner != sender.uniqueId) { val ownerName = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() - throw InvalidCommandArgument("You are not the owner of this Chat-Bubble! ($ownerName is.)") + throw InvalidCommandArgument( + "You must be the owner of the bubble to use this command. (Current owner: $ownerName)" + ) } bubble } @@ -50,10 +52,10 @@ private class BubbleCommand( @CommandAlias("blow") fun create(sender: Player) { if (bubbleManager.getBubbleByPlayer(sender) != null) - throw ChattoreException("You are already in a Chat-Bubble!") + throw ChattoreException("You are already in a bubble!") bubbleManager.createBubble(sender.uniqueId) addExcluded(sender.uniqueId) - sender.sendInfo("Successfully created Chat-Bubble!") + sender.sendInfo("Bubble created.") } @Subcommand("invite") @@ -62,32 +64,32 @@ private class BubbleCommand( if (player.uniqueId in bubble.invitedPlayers) throw ChattoreException("${player.username} is already invited!") if (player.uniqueId in bubble.players) - throw ChattoreException("${player.username} is already in the Chat-Bubble!") + throw ChattoreException("${player.username} is already in the bubble!") bubble.invitedPlayers.add(player.uniqueId) - sender.sendInfo("Successfully invited ${player.username}!") - player.sendInfo("${sender.username} invited you to their Chat-Bubble!") + sender.sendInfo("Invited ${player.username}.") + player.sendInfo("${sender.username} invited you to their bubble.") } @Subcommand("join") fun join(sender: Player, target: OnlinePlayer) { if (bubbleManager.getBubbleByPlayer(sender) != null) - throw ChattoreException("You are already in a Chat-Bubble!") + throw ChattoreException("You are already in a bubble!") val player = target.player val bubble = bubbleManager.getBubbleByPlayer(player) - ?: throw ChattoreException("Target is not in a Chat-Bubble!") + ?: throw ChattoreException("${player.username} is not in a bubble!") if (bubble.isPrivate && sender.uniqueId !in bubble.invitedPlayers) - throw ChattoreException("You were not invited to the Chat-Bubble!") + throw ChattoreException("You are not invited to the bubble!") bubble.invitedPlayers.remove(sender.uniqueId) bubble.players.add(sender.uniqueId) addExcluded(sender.uniqueId) bubble.sendInfos( sender, - "You successfully joined ${player.username}'s Chat-Bubble!", - "${sender.username} joined your Chat-Bubble!", + "You joined ${player.username}'s bubble.", + "${sender.username} joined the bubble.", ) } @@ -95,17 +97,17 @@ private class BubbleCommand( fun leave(sender: Player, bubble: Bubble) { bubble.players.remove(sender.uniqueId) messenger.excludedFromGlobalChat.remove(sender.uniqueId) - sender.sendInfo("You successfully left the Chat-Bubble!") + sender.sendInfo("You left the bubble.") if (bubble.players.isEmpty()) { bubbleManager.removeBubble(bubble) return } + bubble.broadcastInfo("${sender.username} left the bubble.") if (bubble.owner == sender.uniqueId) { val newOwner = bubble.players.first() bubble.owner = newOwner - proxy.playerOrNull(newOwner)?.sendInfo("You are now the owner of the Chat-Bubble!") + proxy.playerOrNull(newOwner)?.sendInfo("You are now the owner of the bubble.") } - bubble.broadcastInfo("${sender.username} left your Chat-Bubble!") } @Subcommand("delete") @@ -113,8 +115,8 @@ private class BubbleCommand( fun delete(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble) { bubble.sendInfos( sender, - "You popped the the Chat-Bubble!", - "${sender.username} popped your Chat-Bubble!", + "You popped the bubble.", + "${sender.username} popped the bubble.", ) messenger.excludedFromGlobalChat.removeAll(bubble.players) bubbleManager.removeBubble(bubble) @@ -128,11 +130,11 @@ private class BubbleCommand( bubble.players.remove(player.uniqueId) messenger.excludedFromGlobalChat.remove(player.uniqueId) - player.sendInfo("You were kicked out of the Chat-Bubble!") + player.sendInfo("You were kicked out of the bubble.") bubble.sendInfos( sender, - "You kicked ${player.username} from the Chat-Bubble!", - "${sender.username} kicked ${player.username} from your Chat-Bubble!", + "You kicked ${player.username} from the bubble.", + "${sender.username} kicked ${player.username} from the bubble.", ) } @@ -143,15 +145,15 @@ private class BubbleCommand( val visibility = if (isPrivate) "private" else "public" bubble.sendInfos( sender, - "The Chat-Bubble is now $visibility!", - "${sender.username} set the Chat-Bubble to $visibility!", + "The bubble is now $visibility.", + "${sender.username} set the bubble to $visibility.", ) } @Subcommand("list") fun list(sender: Player) { if (bubbleManager.bubbles.isEmpty()) { - sender.sendInfo("There are currently no Chat-Bubbles!") + sender.sendInfo("There are currently no bubbles.") return } sender.sendRichMessage("Bubbles:") @@ -170,22 +172,21 @@ private class BubbleCommand( } } - @Subcommand("forcedelete") - @CommandAlias("burst") + @Subcommand("burst") @CommandPermission("chattore.bubble.manage") - fun forcedelete(sender: Player, target: OnlinePlayer) { + fun burst(sender: Player, target: OnlinePlayer) { val player = target.player val bubble = bubbleManager.getBubbleByPlayer(player) - ?: throw ChattoreException("Target is not in a Chat-Bubble!") - bubble.broadcastInfo("The Chat-Bubble was burst by ${sender.username}!") + ?: throw ChattoreException("${player.username} is not in a bubble!") + bubble.broadcastInfo("The bubble was burst by ${sender.username}.") messenger.excludedFromGlobalChat.removeAll(bubble.players) bubbleManager.removeBubble(bubble) - sender.sendInfo("You successfully burst ${player.username}'s Chat-Bubble!") + sender.sendInfo("You burst ${player.username}'s bubble.") } @CommandAlias("showglobalchat|gc") @CommandCompletion("true|false") - fun seeGlobalChat(sender: Player, showGlobalChat: Boolean) { + fun showGlobalChat(sender: Player, showGlobalChat: Boolean) { database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, showGlobalChat) if (showGlobalChat) { messenger.excludedFromGlobalChat.remove(sender.uniqueId) @@ -193,8 +194,8 @@ private class BubbleCommand( messenger.excludedFromGlobalChat.add(sender.uniqueId) } sender.sendInfo( - if (showGlobalChat) "You will now see global chat inside your Chat-Bubble!" - else "You won't see global chat inside your Chat-Bubble anymore!" + if (showGlobalChat) "You will now see global chat inside bubbles." + else "You will no longer see global chat inside bubbles." ) } @@ -229,7 +230,7 @@ class Bubble( .joinToString(", ") fun formatInfo(proxy: ProxyServer) = - HoverEvent.showText(Component.text("In a bubble with: ${playersString(proxy)}")) + HoverEvent.showText(Component.text("Bubble: ${playersString(proxy)}")) } class BubbleManager { From 0e1571f8b98036fab2d3419d4f1c35294fbf50cb Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 09:27:13 +0300 Subject: [PATCH 26/63] Add join button to bubble invites --- chattore/src/main/kotlin/Util.kt | 6 +++--- chattore/src/main/kotlin/feature/Bubble.kt | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/chattore/src/main/kotlin/Util.kt b/chattore/src/main/kotlin/Util.kt index 8347cce..b2adff4 100644 --- a/chattore/src/main/kotlin/Util.kt +++ b/chattore/src/main/kotlin/Util.kt @@ -63,9 +63,9 @@ fun Audience.sendSimpleS(format: String, message: String) = sendSimpleC(format, fun Audience.sendSimpleMM(format: String, message: String) = sendSimpleC(format, message.render()) private const val infoFormat = "[ChattORE] " -fun Audience.sendInfo(message: String) = sendSimpleC(infoFormat, message.toComponent()) -fun Audience.sendInfoMM(message: String, vararg resolvers: TagResolver) = - sendSimpleC(infoFormat, message.render(*resolvers)) +fun Audience.sendInfoC(message: Component) = sendSimpleC(infoFormat, message) +fun Audience.sendInfo(message: String) = sendInfoC(message.toComponent()) +fun Audience.sendInfoMM(message: String, vararg resolvers: TagResolver) = sendInfoC(message.render(*resolvers)) /** Mirrors Player.sendRichMessage **/ fun Audience.sendRichMessage(message: String, vararg resolvers: TagResolver) = sendMessage(message.render(*resolvers)) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 661855e..06b660c 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -29,7 +29,7 @@ fun PluginScope.createBubbleFeature( if (ctx.hasFlag(BUBBLE_OWNED) && bubble.owner != sender.uniqueId) { val ownerName = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() throw InvalidCommandArgument( - "You must be the owner of the bubble to use this command. (Current owner: $ownerName)" + "You must be the owner of the bubble to use this command. (Current owner: $ownerName)", ) } bubble @@ -68,7 +68,9 @@ private class BubbleCommand( bubble.invitedPlayers.add(player.uniqueId) sender.sendInfo("Invited ${player.username}.") - player.sendInfo("${sender.username} invited you to their bubble.") + player.sendInfoC( + "${sender.username} invited you to their bubble. ".toComponent().append(bubble.joinButton(proxy)), + ) } @Subcommand("join") @@ -158,15 +160,11 @@ private class BubbleCommand( } sender.sendRichMessage("Bubbles:") for (bubble in bubbleManager.bubbles) { - val owner = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() - val joinButton = "[Join]".render() - .clickEvent(ClickEvent.suggestCommand("/bubble join $owner")) - .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) sender.sendMessage( Component.textOfChildren( Component.text(bubble.playersString(proxy)), " | ".render(), - joinButton, + bubble.joinButton(proxy), ) ) } @@ -231,6 +229,13 @@ class Bubble( fun formatInfo(proxy: ProxyServer) = HoverEvent.showText(Component.text("Bubble: ${playersString(proxy)}")) + + fun joinButton(proxy: ProxyServer): Component { + val ownerName = proxy.playerOrNull(owner)?.username ?: owner.toString() + return "[Join]".render() + .clickEvent(ClickEvent.runCommand("/bubble join $ownerName")) + .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) + } } class BubbleManager { From d40cfd226a4c03e4a3a820f1f9e76b6154132d5b Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sun, 31 May 2026 09:37:48 +0300 Subject: [PATCH 27/63] Hide Discord chat when in bubble and showGlobalChat is off --- chattore/src/main/kotlin/Messenger.kt | 16 ++++++++-------- chattore/src/main/kotlin/feature/Discord.kt | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 9ffe281..d829ec2 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -85,17 +85,17 @@ class Messenger( return sender to prefix.legacyDeserialize() } + val globalChatReceivers = proxy.all { it.uniqueId !in excludedFromGlobalChat } + fun broadcastChatMessage(originServer: String, player: Player, message: String) { val (sender, compoPrefix) = senderAndPrefix(player) - proxy.allPlayers.filter { it.uniqueId !in excludedFromGlobalChat }.forEach { - it.sendRichMessage( - formatConfig.global, - "message" toC prepareChatMessage(message, player), - "sender" toC sender, - "prefix" toC compoPrefix, - ) - } + globalChatReceivers.sendRichMessage( + formatConfig.global, + "message" toC prepareChatMessage(message, player), + "sender" toC sender, + "prefix" toC compoPrefix, + ) val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix) val discordBroadcast = DiscordBroadcastEvent( diff --git a/chattore/src/main/kotlin/feature/Discord.kt b/chattore/src/main/kotlin/feature/Discord.kt index 088357f..bf2a212 100644 --- a/chattore/src/main/kotlin/feature/Discord.kt +++ b/chattore/src/main/kotlin/feature/Discord.kt @@ -60,7 +60,7 @@ fun PluginScope.createDiscordFeature( val textChannel = discordNetwork.getTextChannelById(config.channelId).getOrNull() ?: throw ChattoreException("Cannot find Discord channel") textChannel.addMessageCreateListener( - DiscordListener(logger, messenger, proxy, emojis, config) + DiscordListener(logger, messenger, emojis, config) ) registerListeners(DiscordBroadcastListener(config, discordMap, discordNetwork)) } @@ -99,7 +99,6 @@ private class DiscordBroadcastListener( private class DiscordListener( private val logger: Logger, private val messenger: Messenger, - private val proxy: ProxyServer, private val emojis: Emojis, private val config: DiscordConfig, ) : MessageCreateListener { @@ -124,7 +123,7 @@ private class DiscordListener( val url = matchResult.groupValues[2].trim() "$text: $url" }.replace("""\s+""".toRegex(), " ") - proxy.all.sendRichMessage( + messenger.globalChatReceivers.sendRichMessage( config.ingameFormat, "sender" toS event.messageAuthor.displayName, "message" toC messenger.prepareChatMessage(transformedMessage, null), From 04616659d9e524bbfaad74146feee11ee7c915f4 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:38:12 +0300 Subject: [PATCH 28/63] Tweak bubble commands and update README accordingly --- README.md | 28 +++++++++++----------- chattore/src/main/kotlin/feature/Bubble.kt | 10 ++++---- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ca67ebf..80f2bd5 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,21 @@ Because we want to have a chat system that actually wOREks for us. | `/profile about ` | `chattore.profile.about` | Set your about | `/playerprofile` | | `/profile setabout ` | `chattore.profile.about.others` | Set another player's about | `/playerprofile` | -## Chat-Bubble Commands +## Bubble Commands -| Command | Permission | Description | Aliases | -|--------------------------------|--------------------------|-----------------------------------------------------------------|---------------------------| -| `/bubble create` | `chattore.bubble` | Create ("Blow") a Chat-Bubble | `/bb create\|/blow` | -| `/bubble invite ` | `chattore.bubble` | Invite Players to your (private) Chat-Bubble | `/bb invite` | -| `/bubble join ` | `chattore.bubble` | Join a Player's Chat-Bubble | `/bb join` | -| `/bubble leave` | `chattore.bubble` | Leave your current Chat-Bubble | `/bb leave` | -| `/bubble delete` | `chattore.bubble` | Delete ("Pop") your current Chat-Bubble | `/bb delete\|/pop` | -| `/bubble kick ` | `chattore.bubble` | Kick a Player from your own Chat-Bubble | `/bb kick` | -| `/bubble setPrivate ` | `chattore.bubble` | Set the visibility of your own Chat-Bubble | `/bb private` | -| `/bubble list` | `chattore.bubble` | List all Chat-Bubbles | `/bb list\|/bubbles` | -| `/shout ` | `chattore.bubble` | Send a message to global chat when inside a Chat-Bubble | No aliases | -| `/bubble forcedelete ` | `chattore.bubble.manage` | Force-Delete ("Burst") a specific Chat-Bubble | `/bb forcedelete\|/burst` | -| `seeGlobalChat ` | `chattore.bubble` | Toggles the visibility of global chat when inside a Chat-Bubble | `/gc` | +| Command | Permission | Description | Aliases | +|------------------------------------|--------------------------|-----------------------------------------------------------|------------------------| +| `/bubble create` | `chattore.bubble` | Create ("blow") a bubble | `/bb create\|/bb blow` | +| `/bubble invite ` | `chattore.bubble` | Invite a player to your bubble (if it is private) | `/bb invite` | +| `/bubble join ` | `chattore.bubble` | Join a player's bubble | `/bb join` | +| `/bubble leave` | `chattore.bubble` | Leave your current bubble | `/bb leave` | +| `/bubble delete` | `chattore.bubble` | Delete ("pop") your own bubble | `/bb delete\|/bb pop` | +| `/bubble kick ` | `chattore.bubble` | Kick a player from your own bubble | `/bb kick` | +| `/bubble setprivate ` | `chattore.bubble` | Set the visibility of your own bubble | `/bb setprivate` | +| `/bubble list` | `chattore.bubble` | List all bubbles | `/bb list` | +| `/shout ` | `chattore.bubble` | Send a message to global chat when inside a bubble | No aliases | +| `/bubble burst ` | `chattore.bubble.manage` | Burst (delete) someone's bubble | `/bb burst` | +| `/bubble showglobalchat ` | `chattore.bubble` | Toggle the visibility of global chat when inside a bubble | `/bb sgc` | ## Other Commands diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 06b660c..7e372e0 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -48,8 +48,7 @@ private class BubbleCommand( private var bubbleManager: BubbleManager, ) : BaseCommand() { - @Subcommand("create") - @CommandAlias("blow") + @Subcommand("create|blow") fun create(sender: Player) { if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a bubble!") @@ -112,8 +111,7 @@ private class BubbleCommand( } } - @Subcommand("delete") - @CommandAlias("pop") + @Subcommand("delete|pop") fun delete(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble) { bubble.sendInfos( sender, @@ -140,7 +138,7 @@ private class BubbleCommand( ) } - @Subcommand("setPrivate") + @Subcommand("setprivate") @CommandCompletion("true|false") fun setPrivate(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, isPrivate: Boolean) { bubble.isPrivate = isPrivate @@ -182,7 +180,7 @@ private class BubbleCommand( sender.sendInfo("You burst ${player.username}'s bubble.") } - @CommandAlias("showglobalchat|gc") + @Subcommand("showglobalchat|sgc") @CommandCompletion("true|false") fun showGlobalChat(sender: Player, showGlobalChat: Boolean) { database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, showGlobalChat) From 26e96371d8cf5e618369d18fd869310f7d4c440a Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:17:17 +0300 Subject: [PATCH 29/63] Add bubble command help and command descriptions --- chattore/src/main/kotlin/ChattORE.kt | 2 ++ chattore/src/main/kotlin/feature/Bubble.kt | 25 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index e280a7e..362e0c1 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -39,6 +39,8 @@ class ChattORE @Inject constructor( commandManager.apply { setDefaultExceptionHandler(::handleCommandException, false) commandCompletions.registerCompletion("username") { listOf(it.player.username) } + @Suppress("DEPRECATION") + enableUnstableAPI("help") } pluginScope.apply { val emojis = createEmojiFeature() diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 7e372e0..1847953 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -1,9 +1,11 @@ package org.openredstone.chattore.feature import co.aikar.commands.BaseCommand +import co.aikar.commands.CommandHelp import co.aikar.commands.InvalidCommandArgument import co.aikar.commands.annotation.* import co.aikar.commands.velocity.contexts.OnlinePlayer +import com.velocitypowered.api.command.CommandSource import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component @@ -34,6 +36,9 @@ fun PluginScope.createBubbleFeature( } bubble } + commandCompletions.registerStaticCompletion("boolean", arrayOf("true", "false")) + commandCompletions.setDefaultCompletion("boolean", Boolean::class.java) + commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java) } registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager)) return bubbleManager @@ -48,7 +53,16 @@ private class BubbleCommand( private var bubbleManager: BubbleManager, ) : BaseCommand() { + @CatchUnknown + @HelpCommand + @Subcommand("help") + fun help(sender: CommandSource, help: CommandHelp) { + sender.sendInfoMM("Bubble help") + help.showHelp() + } + @Subcommand("create|blow") + @Description("Create (\"blow\") a bubble") fun create(sender: Player) { if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a bubble!") @@ -58,6 +72,7 @@ private class BubbleCommand( } @Subcommand("invite") + @Description("Invite a player to your bubble (if it is private)") fun invite(sender: Player, bubble: Bubble, target: OnlinePlayer) { val player = target.player if (player.uniqueId in bubble.invitedPlayers) @@ -73,6 +88,7 @@ private class BubbleCommand( } @Subcommand("join") + @Description("Join a player's bubble") fun join(sender: Player, target: OnlinePlayer) { if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a bubble!") @@ -95,6 +111,7 @@ private class BubbleCommand( } @Subcommand("leave") + @Description("Leave your current bubble") fun leave(sender: Player, bubble: Bubble) { bubble.players.remove(sender.uniqueId) messenger.excludedFromGlobalChat.remove(sender.uniqueId) @@ -112,6 +129,7 @@ private class BubbleCommand( } @Subcommand("delete|pop") + @Description("Delete (\"pop\") your own bubble") fun delete(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble) { bubble.sendInfos( sender, @@ -123,6 +141,7 @@ private class BubbleCommand( } @Subcommand("kick") + @Description("Kick a player from your own bubble") fun kick(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, target: OnlinePlayer) { val player = target.player if (player.uniqueId == sender.uniqueId) @@ -139,7 +158,7 @@ private class BubbleCommand( } @Subcommand("setprivate") - @CommandCompletion("true|false") + @Description("Set the visibility of your own bubble") fun setPrivate(sender: Player, @Flags(BUBBLE_OWNED) bubble: Bubble, isPrivate: Boolean) { bubble.isPrivate = isPrivate val visibility = if (isPrivate) "private" else "public" @@ -151,6 +170,7 @@ private class BubbleCommand( } @Subcommand("list") + @Description("List all bubbles") fun list(sender: Player) { if (bubbleManager.bubbles.isEmpty()) { sender.sendInfo("There are currently no bubbles.") @@ -170,6 +190,7 @@ private class BubbleCommand( @Subcommand("burst") @CommandPermission("chattore.bubble.manage") + @Description("Burst (delete) someone's bubble") fun burst(sender: Player, target: OnlinePlayer) { val player = target.player val bubble = bubbleManager.getBubbleByPlayer(player) @@ -181,7 +202,7 @@ private class BubbleCommand( } @Subcommand("showglobalchat|sgc") - @CommandCompletion("true|false") + @Description("Toggle the visibility of global chat when inside a bubble") fun showGlobalChat(sender: Player, showGlobalChat: Boolean) { database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, showGlobalChat) if (showGlobalChat) { From 1048382f2793a2d1f92ab1b0b0ccbc730979a42d Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:05:57 +0300 Subject: [PATCH 30/63] Add bubble chat to commandspy --- chattore/src/main/kotlin/ChattORE.kt | 4 +- chattore/src/main/kotlin/ChattOREConfig.kt | 4 +- chattore/src/main/kotlin/Config.kt | 9 ++++ chattore/src/main/kotlin/Messenger.kt | 59 +++++++++++++--------- chattore/src/main/kotlin/feature/Bubble.kt | 4 +- chattore/src/main/kotlin/feature/Spying.kt | 23 ++++----- 6 files changed, 60 insertions(+), 43 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 362e0c1..b3a557a 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -45,7 +45,8 @@ class ChattORE @Inject constructor( pluginScope.apply { val emojis = createEmojiFeature() val userCache = createUserCache(database.database) - val messenger = createMessenger(emojis, database, luckPerms, config.format) + val spies = createSpyingFeature(database) + val messenger = createMessenger(emojis, database, luckPerms, config.format, spies) val bubbleManager = createBubbleFeature(messenger, database) createAliasFeature() createChatFeature( @@ -68,7 +69,6 @@ class ChattORE @Inject constructor( ) ) createProfileFeature(database, luckPerms, userCache) - createSpyingFeature(database) } } diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index 0e19486..7731c25 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -15,10 +15,10 @@ data class ChattOREConfig( ) data class FormatConfig( - val global: String = " | : ", + val chatMessage: String = " | : ", val join: String = " has joined the network", val leave: String = " has left the network", - val bubbleChat: String = "[B] | : ", + val bubblePrefix: String = "[B] ", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", ) diff --git a/chattore/src/main/kotlin/Config.kt b/chattore/src/main/kotlin/Config.kt index efb537d..9c01593 100644 --- a/chattore/src/main/kotlin/Config.kt +++ b/chattore/src/main/kotlin/Config.kt @@ -99,8 +99,17 @@ private val removeUnnecessaryStuffAndReorganize: Migration = { } } +private val bubbleIntroduction: Migration = { + // format.global -> format.chatMessage + getObject("format").apply { + replace("chatMessage", get("global")) + remove("global") + } +} + private val migrations = arrayOf( removeUnnecessaryStuffAndReorganize, + bubbleIntroduction, ) private val currentConfigVersion: ConfigVersion = migrations.size diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index d829ec2..e911eef 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextReplacementConfig import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder @@ -24,11 +25,12 @@ fun PluginScope.createMessenger( database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, + spies: Audience, ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, spies) } class Messenger( @@ -38,6 +40,7 @@ class Messenger( private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, private val fileTypeMap: Map>, + private val spies: Audience, ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -72,30 +75,35 @@ class Messenger( } .build() - private fun senderAndPrefix(player: Player): Pair { - val userId = player.uniqueId - val name = database.getNickname(userId) ?: NickPreset(player.username) - val sender = - "Click for more'>" - .renderSimpleC(name.render(player.username)) - - val luckUser = luckPerms.userManager.getUser(userId)!! // online users guaranteed to be loaded + private fun formatPrefix(player: Player): Component { + val luckUser = luckPerms.userManager.getUser(player.uniqueId)!! // online users guaranteed to be loaded val prefix = luckUser.cachedData.metaData.prefix ?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar) - return sender to prefix.legacyDeserialize() + return prefix.legacyDeserialize() + } + + private fun formatSender(player: Player): Component { + val name = database.getNickname(player.uniqueId) ?: NickPreset(player.username) + return "Click for more'>" + .renderSimpleC(name.render(player.username)) } + private fun formatChatMessage( + message: String, + player: Player, + sender: Component = formatSender(player), + prefix: Component = formatPrefix(player), + ) = formatConfig.chatMessage.render( + "message" toC prepareChatMessage(message, player), + "sender" toC sender, + "prefix" toC prefix, + ) + val globalChatReceivers = proxy.all { it.uniqueId !in excludedFromGlobalChat } fun broadcastChatMessage(originServer: String, player: Player, message: String) { - val (sender, compoPrefix) = senderAndPrefix(player) - - globalChatReceivers.sendRichMessage( - formatConfig.global, - "message" toC prepareChatMessage(message, player), - "sender" toC sender, - "prefix" toC compoPrefix, - ) + val compoPrefix = formatPrefix(player) + globalChatReceivers.sendMessage(formatChatMessage(message, player, prefix = compoPrefix)) val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix) val discordBroadcast = DiscordBroadcastEvent( @@ -108,16 +116,17 @@ class Messenger( } fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { - val (sender, compoPrefix) = senderAndPrefix(player) + val formattedMessage = formatChatMessage(message, player) + val bubbleInfo = Placeholder.styling("bubbleinfo", bubble.formatInfo(proxy)) bubble.players.forEach { uuid -> - proxy.playerOrNull(uuid)?.sendRichMessage( - formatConfig.bubbleChat, - "message" toC prepareChatMessage(message, player), - "sender" toC sender, - "prefix" toC compoPrefix, - Placeholder.styling("bubbleinfo", bubble.formatInfo(proxy)), + proxy.playerOrNull(uuid)?.sendMessage( + formatConfig.bubblePrefix.render(bubbleInfo).append(formattedMessage) ) } + spies.sendMessage( + "[Spy] ".render(bubbleInfo) + .append(formattedMessage) + ) } fun prepareChatMessage( diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 1847953..aa562b4 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -49,8 +49,8 @@ fun PluginScope.createBubbleFeature( private class BubbleCommand( private val messenger: Messenger, private val proxy: ProxyServer, - private var database: Storage, - private var bubbleManager: BubbleManager, + private val database: Storage, + private val bubbleManager: BubbleManager, ) : BaseCommand() { @CatchUnknown diff --git a/chattore/src/main/kotlin/feature/Spying.kt b/chattore/src/main/kotlin/feature/Spying.kt index a096165..e0b8571 100644 --- a/chattore/src/main/kotlin/feature/Spying.kt +++ b/chattore/src/main/kotlin/feature/Spying.kt @@ -7,7 +7,6 @@ import co.aikar.commands.annotation.Default import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.command.CommandExecuteEvent import com.velocitypowered.api.proxy.Player -import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.audience.Audience import org.openredstone.chattore.* @@ -15,25 +14,25 @@ private val SpyEnabled = Setting("spy") fun PluginScope.createSpyingFeature( database: Storage, -) { +): Audience { + fun Player.isSpying() = database.getSetting(SpyEnabled, uniqueId) ?: false + val spies = proxy.all { it.hasChattorePrivilege && it.isSpying() } registerCommands(CommandSpy(database)) - registerListeners(CommandListener(database, proxy)) + registerListeners(CommandListener(spies)) + return spies } + private class CommandListener( - private val database: Storage, - proxy: ProxyServer, + private val spies: Audience, ) { - private val Player.isSpying: Boolean get() = database.getSetting(SpyEnabled, uniqueId) ?: false - private val spies: Audience = proxy.all { it.hasChattorePrivilege && it.isSpying } @Subscribe fun onCommandEvent(event: CommandExecuteEvent) { - spies.sendMessage( - ": ".render( - "message" toS event.command, - "sender" toS ((event.commandSource as? Player)?.username ?: "Console"), - ) + spies.sendRichMessage( + ": ", + "message" toS event.command, + "sender" toS ((event.commandSource as? Player)?.username ?: "Console"), ) } } From 9da2277a1bfd2c86ea1fbba1faf158f7efc17b29 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:46:39 +0300 Subject: [PATCH 31/63] Refactor chat confirmations to fix bubble confirms going to global chat --- chattore/src/main/kotlin/feature/Chat.kt | 99 +++++++++++++----------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index ac05282..9a25b4f 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -24,50 +24,29 @@ fun PluginScope.createChatFeature( config: ChatConfirmationConfig, bubbleManager: BubbleManager, ) { - val flaggedMessages = ConcurrentHashMap() - val chatListener = ChatListener(config, flaggedMessages, logger, messenger, bubbleManager) + val confirmations = ChatConfirmations(config, logger) + val chatListener = ChatListener(confirmations, logger, messenger, bubbleManager) registerListeners(chatListener) - registerCommands(ConfirmMessage(flaggedMessages, logger, messenger), Shout(chatListener)) + registerCommands(confirmations.ConfirmMessage(), Shout(chatListener, confirmations)) } -private class ChatListener( +class ChatConfirmations( config: ChatConfirmationConfig, - private val flaggedMessages: ConcurrentHashMap, private val logger: Logger, - private val messenger: Messenger, - private val bubbleManager: BubbleManager, ) { - private val regexes = config.regexes.mapNotNull { pattern -> runCatching { Regex(pattern, RegexOption.IGNORE_CASE) } .onFailure { logger.error("Invalid regex $pattern: ${it.message}") } .getOrNull() } - @Subscribe - fun onChatEvent(event: PlayerChatEvent) { - val player = event.player - val message = event.message - handleChat(player, message, bubbleManager.getBubbleByPlayer(player)) - } - - fun handleChat(player: Player, message: String, bubble: Bubble?) { - if (isFlagged(player, message)) return - logger.info("${player.username} (${player.uniqueId}): $message") - player.currentServer.ifPresent { server -> - if (bubble != null) { - messenger.broadcastBubbleMessage(player, message, bubble) - } else { - messenger.broadcastChatMessage(server.serverInfo.name, player, message) - } - } - } - - private fun isFlagged(player: Player, message: String): Boolean { + private val flags = ConcurrentHashMap Unit>>() + fun submit(player: Player, message: String, proceed: () -> Unit) { val matches = regexes.filter { it.containsMatchIn(message) } if (matches.isEmpty()) { - flaggedMessages.remove(player.uniqueId) - return false + flags.remove(player.uniqueId) + proceed() + return } fun String.highlight(r: Regex) = r.replace(this) { match -> "${match.value}" } val highlighted = matches.fold(message, String::highlight) @@ -78,26 +57,53 @@ private class ChatListener( "/confirmmessage.", highlighted, ) - flaggedMessages[player.uniqueId] = message - return true + flags[player.uniqueId] = message to proceed + return + } + + @CommandAlias("confirmmessage") + @CommandPermission("chattore.confirmmessage") + inner class ConfirmMessage : BaseCommand() { + @Default + fun default(player: Player) { + val (message, proceed) = flags[player.uniqueId] + ?: throw ChattoreException("You have no message to confirm!") + player.sendRichMessage("Override recognized") + flags.remove(player.uniqueId) + logger.info("${player.username} (${player.uniqueId}) FLAGGED MESSAGE OVERRIDE: $message") + proceed() + } } } -@CommandAlias("confirmmessage") -@CommandPermission("chattore.confirmmessage") -private class ConfirmMessage( - private val flaggedMessages: ConcurrentHashMap, +private class ChatListener( + private val confirmations: ChatConfirmations, private val logger: Logger, private val messenger: Messenger, -) : BaseCommand() { - @Default - fun default(player: Player) { - val message = flaggedMessages[player.uniqueId] ?: throw ChattoreException("You have no message to confirm!") - player.sendRichMessage("Override recognized") - flaggedMessages.remove(player.uniqueId) - logger.info("${player.username} (${player.uniqueId}) FLAGGED MESSAGE OVERRIDE: $message") + private val bubbleManager: BubbleManager, +) { + @Subscribe + fun onChatEvent(event: PlayerChatEvent) { + val player = event.player + val message = event.message + confirmations.submit(player, message) { + handleChat(player, message, bubbleManager.getBubbleByPlayer(player)) + } + } + + // NOTE: confirmations.submit is not done here in a single place so that we get + // the bubble the player is in *at the time of confirmation*, not when the message was sent, + // because otherwise you could leave (or be kicked) from your bubble and still send messages there + // if you had something to confirm. Ideally, you wouldn't be able to confirm that anymore, but this'll do for now + fun handleChat(player: Player, message: String, bubble: Bubble?) { player.currentServer.ifPresent { server -> - messenger.broadcastChatMessage(server.serverInfo.name, player, message) + if (bubble != null) { + logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message") + messenger.broadcastBubbleMessage(player, message, bubble) + } else { + logger.info("${player.username} (${player.uniqueId}): $message") + messenger.broadcastChatMessage(server.serverInfo.name, player, message) + } } } } @@ -106,9 +112,12 @@ private class ConfirmMessage( @CommandPermission("chattore.bubble") private class Shout( private val chatListener: ChatListener, + private val confirmations: ChatConfirmations, ) : BaseCommand() { @Default fun shout(sender: Player, message: String) { - chatListener.handleChat(sender, message, bubble = null) // passing bubble = null to shout + confirmations.submit(sender, message) { + chatListener.handleChat(sender, message, bubble = null) + } } } From 1d5bfb523f4005dfb2d996045e2dc8a24c48cf69 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:59:03 +0300 Subject: [PATCH 32/63] Add sendError and recolor info messages --- chattore/src/main/kotlin/ChattORE.kt | 2 +- chattore/src/main/kotlin/Util.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index b3a557a..417ec19 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -92,7 +92,7 @@ class ChattORE @Inject constructor( val exception = throwable as? ChattoreException ?: return false val message = exception.message ?: "Something went wrong!" // cast ok because we're running on Velocity - (sender as VelocityCommandIssuer).issuer.sendInfoMM("", "message" toS message) + (sender as VelocityCommandIssuer).issuer.sendError(message) return true } // TODO reloading functionality diff --git a/chattore/src/main/kotlin/Util.kt b/chattore/src/main/kotlin/Util.kt index b2adff4..5f9d04e 100644 --- a/chattore/src/main/kotlin/Util.kt +++ b/chattore/src/main/kotlin/Util.kt @@ -62,11 +62,13 @@ fun Audience.sendSimpleC(format: String, message: Component) = sendMessage(forma fun Audience.sendSimpleS(format: String, message: String) = sendSimpleC(format, message.toComponent()) fun Audience.sendSimpleMM(format: String, message: String) = sendSimpleC(format, message.render()) -private const val infoFormat = "[ChattORE] " +private const val infoFormat = "[ChattORE] " fun Audience.sendInfoC(message: Component) = sendSimpleC(infoFormat, message) fun Audience.sendInfo(message: String) = sendInfoC(message.toComponent()) fun Audience.sendInfoMM(message: String, vararg resolvers: TagResolver) = sendInfoC(message.render(*resolvers)) +fun Audience.sendError(message: String) = sendInfoMM("", "message" toS message) + /** Mirrors Player.sendRichMessage **/ fun Audience.sendRichMessage(message: String, vararg resolvers: TagResolver) = sendMessage(message.render(*resolvers)) From a12946494c7f7f88517b228970fd65577661627b Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:01:28 +0300 Subject: [PATCH 33/63] Prevent confirmations from proceeding when bubble no longer exists --- chattore/src/main/kotlin/feature/Chat.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 9a25b4f..bd2a82a 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -7,10 +7,7 @@ import co.aikar.commands.annotation.Default import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.PlayerChatEvent import com.velocitypowered.api.proxy.Player -import org.openredstone.chattore.ChattoreException -import org.openredstone.chattore.Messenger -import org.openredstone.chattore.PluginScope -import org.openredstone.chattore.sendSimpleMM +import org.openredstone.chattore.* import org.slf4j.Logger import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -86,15 +83,16 @@ private class ChatListener( fun onChatEvent(event: PlayerChatEvent) { val player = event.player val message = event.message + val bubble = bubbleManager.getBubbleByPlayer(player) confirmations.submit(player, message) { - handleChat(player, message, bubbleManager.getBubbleByPlayer(player)) + if (bubbleManager.getBubbleByPlayer(player) !== bubble) { + player.sendError("You are no longer in the bubble you're trying to send a message to") + return@submit + } + handleChat(player, message, bubble) } } - // NOTE: confirmations.submit is not done here in a single place so that we get - // the bubble the player is in *at the time of confirmation*, not when the message was sent, - // because otherwise you could leave (or be kicked) from your bubble and still send messages there - // if you had something to confirm. Ideally, you wouldn't be able to confirm that anymore, but this'll do for now fun handleChat(player: Player, message: String, bubble: Bubble?) { player.currentServer.ifPresent { server -> if (bubble != null) { From f71d89a69b2e552915a7d5adf60cbd003e518253 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:22:56 +0300 Subject: [PATCH 34/63] Handle global chat confirmations uniformly --- chattore/src/main/kotlin/Messenger.kt | 10 ++++++++-- chattore/src/main/kotlin/feature/Chat.kt | 25 +++++++++--------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index e911eef..3be59cd 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -16,9 +16,11 @@ import org.openredstone.chattore.feature.Bubble import org.openredstone.chattore.feature.DiscordBroadcastEvent import org.openredstone.chattore.feature.Emojis import org.openredstone.chattore.feature.NickPreset +import org.slf4j.Logger import java.net.URI import java.util.* import java.util.concurrent.ConcurrentHashMap +import kotlin.jvm.optionals.getOrNull fun PluginScope.createMessenger( emojis: Emojis, @@ -30,7 +32,7 @@ fun PluginScope.createMessenger( val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, spies) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, spies, logger) } class Messenger( @@ -41,6 +43,7 @@ class Messenger( private val formatConfig: FormatConfig, private val fileTypeMap: Map>, private val spies: Audience, + private val logger: Logger, ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -101,7 +104,9 @@ class Messenger( val globalChatReceivers = proxy.all { it.uniqueId !in excludedFromGlobalChat } - fun broadcastChatMessage(originServer: String, player: Player, message: String) { + fun broadcastChatMessage(player: Player, message: String) { + logger.info("${player.username} (${player.uniqueId}): $message") + val originServer = player.currentServer.getOrNull()?.serverInfo?.name ?: "VOID" val compoPrefix = formatPrefix(player) globalChatReceivers.sendMessage(formatChatMessage(message, player, prefix = compoPrefix)) @@ -116,6 +121,7 @@ class Messenger( } fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { + logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message") val formattedMessage = formatChatMessage(message, player) val bubbleInfo = Placeholder.styling("bubbleinfo", bubble.formatInfo(proxy)) bubble.players.forEach { uuid -> diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index bd2a82a..34d320d 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -24,7 +24,7 @@ fun PluginScope.createChatFeature( val confirmations = ChatConfirmations(config, logger) val chatListener = ChatListener(confirmations, logger, messenger, bubbleManager) registerListeners(chatListener) - registerCommands(confirmations.ConfirmMessage(), Shout(chatListener, confirmations)) + registerCommands(confirmations.ConfirmMessage(), Shout(chatListener)) } class ChatConfirmations( @@ -84,25 +84,21 @@ private class ChatListener( val player = event.player val message = event.message val bubble = bubbleManager.getBubbleByPlayer(player) + if (bubble == null) { + handleGlobalChat(player, message) + return + } confirmations.submit(player, message) { if (bubbleManager.getBubbleByPlayer(player) !== bubble) { player.sendError("You are no longer in the bubble you're trying to send a message to") return@submit } - handleChat(player, message, bubble) + messenger.broadcastBubbleMessage(player, message, bubble) } } - fun handleChat(player: Player, message: String, bubble: Bubble?) { - player.currentServer.ifPresent { server -> - if (bubble != null) { - logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message") - messenger.broadcastBubbleMessage(player, message, bubble) - } else { - logger.info("${player.username} (${player.uniqueId}): $message") - messenger.broadcastChatMessage(server.serverInfo.name, player, message) - } - } + fun handleGlobalChat(player: Player, message: String) = confirmations.submit(player, message) { + messenger.broadcastChatMessage(player, message) } } @@ -110,12 +106,9 @@ private class ChatListener( @CommandPermission("chattore.bubble") private class Shout( private val chatListener: ChatListener, - private val confirmations: ChatConfirmations, ) : BaseCommand() { @Default fun shout(sender: Player, message: String) { - confirmations.submit(sender, message) { - chatListener.handleChat(sender, message, bubble = null) - } + chatListener.handleGlobalChat(sender, message) } } From 9600c184699953212f07834d89e057cafdc33273 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:27:32 +0300 Subject: [PATCH 35/63] Bump version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bcd1bad..6e3afa4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.2 +version=1.3 group=org.openredstone.chattore From 2a383bff0cf7989789af9787eaecd6bdb45fcd83 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:53:04 +0300 Subject: [PATCH 36/63] Small cleanups --- chattore/src/main/kotlin/ChattORE.kt | 2 +- chattore/src/main/kotlin/Config.kt | 2 +- chattore/src/main/kotlin/feature/Chat.kt | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 417ec19..7e3deab 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -27,7 +27,7 @@ import kotlin.io.path.exists class ChattORE @Inject constructor( private val proxy: ProxyServer, private val logger: Logger, - @DataDirectory private val dataFolder: Path, + @param:DataDirectory private val dataFolder: Path, ) { @Subscribe fun onProxyInitialization(event: ProxyInitializeEvent) { diff --git a/chattore/src/main/kotlin/Config.kt b/chattore/src/main/kotlin/Config.kt index 9c01593..49704d5 100644 --- a/chattore/src/main/kotlin/Config.kt +++ b/chattore/src/main/kotlin/Config.kt @@ -107,7 +107,7 @@ private val bubbleIntroduction: Migration = { } } -private val migrations = arrayOf( +private val migrations = arrayOf( removeUnnecessaryStuffAndReorganize, bubbleIntroduction, ) diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 34d320d..33aac1e 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -22,7 +22,7 @@ fun PluginScope.createChatFeature( bubbleManager: BubbleManager, ) { val confirmations = ChatConfirmations(config, logger) - val chatListener = ChatListener(confirmations, logger, messenger, bubbleManager) + val chatListener = ChatListener(confirmations, messenger, bubbleManager) registerListeners(chatListener) registerCommands(confirmations.ConfirmMessage(), Shout(chatListener)) } @@ -75,7 +75,6 @@ class ChatConfirmations( private class ChatListener( private val confirmations: ChatConfirmations, - private val logger: Logger, private val messenger: Messenger, private val bubbleManager: BubbleManager, ) { From 32d45b78b2711289509480d0b84def83221143ff Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:59:28 +0300 Subject: [PATCH 37/63] Move shout back to Bubble.kt and echo message when global chat disabled --- chattore/src/main/kotlin/ChattORE.kt | 9 ++--- chattore/src/main/kotlin/Messenger.kt | 2 +- chattore/src/main/kotlin/feature/Bubble.kt | 24 ++++++++++--- chattore/src/main/kotlin/feature/Chat.kt | 41 ++++++++-------------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 7e3deab..a02a6b4 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -47,13 +47,10 @@ class ChattORE @Inject constructor( val userCache = createUserCache(database.database) val spies = createSpyingFeature(database) val messenger = createMessenger(emojis, database, luckPerms, config.format, spies) - val bubbleManager = createBubbleFeature(messenger, database) + val chatConfirmations = createChatConfirmations(ChatConfirmationConfig(config.regexes)) + val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations) createAliasFeature() - createChatFeature( - messenger, - ChatConfirmationConfig(config.regexes), - bubbleManager - ) + createChatFeature(messenger, chatConfirmations, bubbleManager) createChattoreFeature() createDiscordFeature(messenger, emojis, config.discord) createFunCommandsFeature() diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 3be59cd..86576bf 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -91,7 +91,7 @@ class Messenger( .renderSimpleC(name.render(player.username)) } - private fun formatChatMessage( + fun formatChatMessage( message: String, player: Player, sender: Component = formatSender(player), diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index aa562b4..57edee5 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -20,6 +20,7 @@ private const val BUBBLE_OWNED = "bubble-owned" fun PluginScope.createBubbleFeature( messenger: Messenger, database: Storage, + chatConfirmations: ChatConfirmations, ): BubbleManager { val bubbleManager = BubbleManager() commandManager.apply { @@ -40,7 +41,7 @@ fun PluginScope.createBubbleFeature( commandCompletions.setDefaultCompletion("boolean", Boolean::class.java) commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java) } - registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager)) + registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager, chatConfirmations)) return bubbleManager } @@ -51,6 +52,7 @@ private class BubbleCommand( private val proxy: ProxyServer, private val database: Storage, private val bubbleManager: BubbleManager, + private val chatConfirmations: ChatConfirmations, ) : BaseCommand() { @CatchUnknown @@ -202,7 +204,7 @@ private class BubbleCommand( } @Subcommand("showglobalchat|sgc") - @Description("Toggle the visibility of global chat when inside a bubble") + @Description("Control the visibility of global chat when in a bubble") fun showGlobalChat(sender: Player, showGlobalChat: Boolean) { database.setSetting(ShowGlobalChatInBubble, sender.uniqueId, showGlobalChat) if (showGlobalChat) { @@ -211,11 +213,25 @@ private class BubbleCommand( messenger.excludedFromGlobalChat.add(sender.uniqueId) } sender.sendInfo( - if (showGlobalChat) "You will now see global chat inside bubbles." - else "You will no longer see global chat inside bubbles." + if (showGlobalChat) "You will now see global chat in bubbles." + else "You will no longer see global chat in bubbles." ) } + @CommandAlias("shout") + @Description("Send a message to global chat when in a bubble") + fun shout(sender: Player, message: String) { + chatConfirmations.submit(sender, message) { + messenger.broadcastChatMessage(sender, message) + if (sender.uniqueId in messenger.excludedFromGlobalChat) { + sender.sendSimpleC( + "[\uD83D\uDD15] ", + messenger.formatChatMessage(message, sender), + ) + } + } + } + private fun Bubble.sendInfos(you: Player, yourMessage: String, theirMessage: String) { players.forEach { uuid -> val player = proxy.playerOrNull(uuid) ?: return@forEach diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 33aac1e..2197785 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -16,16 +16,8 @@ data class ChatConfirmationConfig( val regexes: List = listOf(), ) -fun PluginScope.createChatFeature( - messenger: Messenger, - config: ChatConfirmationConfig, - bubbleManager: BubbleManager, -) { - val confirmations = ChatConfirmations(config, logger) - val chatListener = ChatListener(confirmations, messenger, bubbleManager) - registerListeners(chatListener) - registerCommands(confirmations.ConfirmMessage(), Shout(chatListener)) -} +fun PluginScope.createChatConfirmations(config: ChatConfirmationConfig): ChatConfirmations = + ChatConfirmations(config, logger).also { registerCommands(it.ConfirmMessage()) } class ChatConfirmations( config: ChatConfirmationConfig, @@ -73,6 +65,14 @@ class ChatConfirmations( } } +fun PluginScope.createChatFeature( + messenger: Messenger, + confirmations: ChatConfirmations, + bubbleManager: BubbleManager, +) { + registerListeners(ChatListener(confirmations, messenger, bubbleManager)) +} + private class ChatListener( private val confirmations: ChatConfirmations, private val messenger: Messenger, @@ -84,30 +84,17 @@ private class ChatListener( val message = event.message val bubble = bubbleManager.getBubbleByPlayer(player) if (bubble == null) { - handleGlobalChat(player, message) + confirmations.submit(player, message) { + messenger.broadcastChatMessage(player, message) + } return } confirmations.submit(player, message) { - if (bubbleManager.getBubbleByPlayer(player) !== bubble) { + if (bubbleManager.getBubbleByPlayer(player) != bubble) { player.sendError("You are no longer in the bubble you're trying to send a message to") return@submit } messenger.broadcastBubbleMessage(player, message, bubble) } } - - fun handleGlobalChat(player: Player, message: String) = confirmations.submit(player, message) { - messenger.broadcastChatMessage(player, message) - } -} - -@CommandAlias("shout") -@CommandPermission("chattore.bubble") -private class Shout( - private val chatListener: ChatListener, -) : BaseCommand() { - @Default - fun shout(sender: Player, message: String) { - chatListener.handleGlobalChat(sender, message) - } } From b0a202d078369aadb6f95bf988c4f61064e86e2f Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:00:58 +0300 Subject: [PATCH 38/63] Refactor spying to encapsulate spy prefix into Spying.kt --- chattore/src/main/kotlin/ChattORE.kt | 6 +++--- chattore/src/main/kotlin/ChattOREConfig.kt | 4 +++- chattore/src/main/kotlin/Messenger.kt | 25 ++++++++-------------- chattore/src/main/kotlin/feature/Bubble.kt | 11 ++++++---- chattore/src/main/kotlin/feature/Spying.kt | 13 +++++++---- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index a02a6b4..95a51fd 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -45,10 +45,10 @@ class ChattORE @Inject constructor( pluginScope.apply { val emojis = createEmojiFeature() val userCache = createUserCache(database.database) - val spies = createSpyingFeature(database) - val messenger = createMessenger(emojis, database, luckPerms, config.format, spies) + val wiretap = createSpyingFeature(database, config.format) + val messenger = createMessenger(emojis, database, luckPerms, config.format, wiretap) val chatConfirmations = createChatConfirmations(ChatConfirmationConfig(config.regexes)) - val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations) + val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations, config.format) createAliasFeature() createChatFeature(messenger, chatConfirmations, bubbleManager) createChattoreFeature() diff --git a/chattore/src/main/kotlin/ChattOREConfig.kt b/chattore/src/main/kotlin/ChattOREConfig.kt index 7731c25..cc9ee42 100644 --- a/chattore/src/main/kotlin/ChattOREConfig.kt +++ b/chattore/src/main/kotlin/ChattOREConfig.kt @@ -18,7 +18,9 @@ data class FormatConfig( val chatMessage: String = " | : ", val join: String = " has joined the network", val leave: String = " has left the network", - val bubblePrefix: String = "[B] ", + val bubblePrefix: String = "\uD83D\uDCAC", + val spyPrefix: String = "\uD83D\uDD75", + val shoutPrefix: String = "\uD83D\uDD15", val joinDiscord: String = "**%player% has joined the network**", val leaveDiscord: String = "**%player% has left the network**", ) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 86576bf..0b9f4e2 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -6,16 +6,13 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.space import net.kyori.adventure.text.TextReplacementConfig import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import net.luckperms.api.LuckPerms -import org.openredstone.chattore.feature.Bubble -import org.openredstone.chattore.feature.DiscordBroadcastEvent -import org.openredstone.chattore.feature.Emojis -import org.openredstone.chattore.feature.NickPreset +import org.openredstone.chattore.feature.* import org.slf4j.Logger import java.net.URI import java.util.* @@ -27,12 +24,12 @@ fun PluginScope.createMessenger( database: Storage, luckPerms: LuckPerms, formatConfig: FormatConfig, - spies: Audience, + wiretap: Wiretap, ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, spies, logger) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, wiretap, logger) } class Messenger( @@ -42,7 +39,7 @@ class Messenger( private val luckPerms: LuckPerms, private val formatConfig: FormatConfig, private val fileTypeMap: Map>, - private val spies: Audience, + private val wiretap: Wiretap, private val logger: Logger, ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -123,16 +120,12 @@ class Messenger( fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message") val formattedMessage = formatChatMessage(message, player) - val bubbleInfo = Placeholder.styling("bubbleinfo", bubble.formatInfo(proxy)) + val bubbleInfo = Placeholder.styling("bubble_info", bubble.formatInfo(proxy)) + val renderedMessage = formatConfig.bubblePrefix.render(bubbleInfo).append(space()).append(formattedMessage) bubble.players.forEach { uuid -> - proxy.playerOrNull(uuid)?.sendMessage( - formatConfig.bubblePrefix.render(bubbleInfo).append(formattedMessage) - ) + proxy.playerOrNull(uuid)?.sendMessage(renderedMessage) } - spies.sendMessage( - "[Spy] ".render(bubbleInfo) - .append(formattedMessage) - ) + wiretap(renderedMessage) } fun prepareChatMessage( diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 57edee5..65c6b71 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -9,6 +9,7 @@ import com.velocitypowered.api.command.CommandSource import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.space import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.HoverEvent import org.openredstone.chattore.* @@ -21,6 +22,7 @@ fun PluginScope.createBubbleFeature( messenger: Messenger, database: Storage, chatConfirmations: ChatConfirmations, + formatConfig: FormatConfig, ): BubbleManager { val bubbleManager = BubbleManager() commandManager.apply { @@ -41,7 +43,7 @@ fun PluginScope.createBubbleFeature( commandCompletions.setDefaultCompletion("boolean", Boolean::class.java) commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java) } - registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager, chatConfirmations)) + registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager, chatConfirmations, formatConfig)) return bubbleManager } @@ -53,6 +55,7 @@ private class BubbleCommand( private val database: Storage, private val bubbleManager: BubbleManager, private val chatConfirmations: ChatConfirmations, + private val formatConfig: FormatConfig, ) : BaseCommand() { @CatchUnknown @@ -224,9 +227,9 @@ private class BubbleCommand( chatConfirmations.submit(sender, message) { messenger.broadcastChatMessage(sender, message) if (sender.uniqueId in messenger.excludedFromGlobalChat) { - sender.sendSimpleC( - "[\uD83D\uDD15] ", - messenger.formatChatMessage(message, sender), + sender.sendMessage( + formatConfig.shoutPrefix.render().append(space()) + .append(messenger.formatChatMessage(message, sender)) ) } } diff --git a/chattore/src/main/kotlin/feature/Spying.kt b/chattore/src/main/kotlin/feature/Spying.kt index e0b8571..6e2abdc 100644 --- a/chattore/src/main/kotlin/feature/Spying.kt +++ b/chattore/src/main/kotlin/feature/Spying.kt @@ -8,25 +8,30 @@ import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.command.CommandExecuteEvent import com.velocitypowered.api.proxy.Player import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.space import org.openredstone.chattore.* private val SpyEnabled = Setting("spy") +typealias Wiretap = (Component) -> Unit + fun PluginScope.createSpyingFeature( database: Storage, -): Audience { + formatConfig: FormatConfig, +): Wiretap { + // TODO: this hits the DB way WAY way too often xd fun Player.isSpying() = database.getSetting(SpyEnabled, uniqueId) ?: false val spies = proxy.all { it.hasChattorePrivilege && it.isSpying() } registerCommands(CommandSpy(database)) registerListeners(CommandListener(spies)) - return spies + val spyPrefix = formatConfig.spyPrefix.render().append(space()) + return { secrets -> spies.sendMessage(spyPrefix.append(secrets)) } } - private class CommandListener( private val spies: Audience, ) { - @Subscribe fun onCommandEvent(event: CommandExecuteEvent) { spies.sendRichMessage( From bc6881ff2d47fc4527cce8f69531d9ba1e7ad492 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:24:11 +0300 Subject: [PATCH 39/63] Use bubble manager permission more in commands - bubble list only shows bubbles you can access, unless you have the manage permission - some commands (join) now let you join if you can't access the bubble if you have the manage permission --- chattore/src/main/kotlin/feature/Bubble.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 65c6b71..4e4a5e5 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -102,7 +102,7 @@ private class BubbleCommand( val bubble = bubbleManager.getBubbleByPlayer(player) ?: throw ChattoreException("${player.username} is not in a bubble!") - if (bubble.isPrivate && sender.uniqueId !in bubble.invitedPlayers) + if (bubble.isPrivate && sender.uniqueId !in bubble.invitedPlayers && !sender.hasBubblePrivilege) throw ChattoreException("You are not invited to the bubble!") bubble.invitedPlayers.remove(sender.uniqueId) @@ -174,20 +174,31 @@ private class BubbleCommand( ) } + private fun Player.canSee(bubble: Bubble) = hasBubblePrivilege + || !bubble.isPrivate || uniqueId in bubble.players || uniqueId in bubble.invitedPlayers + + private val Player.hasBubblePrivilege: Boolean get() = hasPermission("chattore.bubble.manage") + @Subcommand("list") @Description("List all bubbles") fun list(sender: Player) { - if (bubbleManager.bubbles.isEmpty()) { + val bubbles = bubbleManager.bubbles.filter { sender.canSee(it) } + if (bubbles.isEmpty()) { sender.sendInfo("There are currently no bubbles.") return } sender.sendRichMessage("Bubbles:") - for (bubble in bubbleManager.bubbles) { + for (bubble in bubbles) { + val info = if (sender.uniqueId in bubble.players) { + "Your current bubble".render() + } else { + bubble.joinButton(proxy) + } sender.sendMessage( Component.textOfChildren( Component.text(bubble.playersString(proxy)), " | ".render(), - bubble.joinButton(proxy), + info, ) ) } From feb7ea690ba307b2ca4436a045238ef90bb520b9 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Mon, 15 Jun 2026 07:13:11 +0300 Subject: [PATCH 40/63] Use UserCache when listing bubble participants --- chattore/src/main/kotlin/ChattORE.kt | 4 ++-- chattore/src/main/kotlin/Messenger.kt | 6 ++++-- chattore/src/main/kotlin/feature/Bubble.kt | 25 ++++++++++++++++------ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 95a51fd..1b4be09 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -46,9 +46,9 @@ class ChattORE @Inject constructor( val emojis = createEmojiFeature() val userCache = createUserCache(database.database) val wiretap = createSpyingFeature(database, config.format) - val messenger = createMessenger(emojis, database, luckPerms, config.format, wiretap) + val messenger = createMessenger(emojis, database, luckPerms, config.format, wiretap, userCache) val chatConfirmations = createChatConfirmations(ChatConfirmationConfig(config.regexes)) - val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations, config.format) + val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations, config.format, userCache) createAliasFeature() createChatFeature(messenger, chatConfirmations, bubbleManager) createChattoreFeature() diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 0b9f4e2..8f5ca41 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -25,11 +25,12 @@ fun PluginScope.createMessenger( luckPerms: LuckPerms, formatConfig: FormatConfig, wiretap: Wiretap, + userCache: UserCache, ): Messenger { val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json")) .jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } } .onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") } - return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, wiretap, logger) + return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, wiretap, logger, userCache) } class Messenger( @@ -41,6 +42,7 @@ class Messenger( private val fileTypeMap: Map>, private val wiretap: Wiretap, private val logger: Logger, + private val userCache: UserCache, ) { private val urlRegex = """]+)?)>?""".toRegex() @@ -120,7 +122,7 @@ class Messenger( fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) { logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message") val formattedMessage = formatChatMessage(message, player) - val bubbleInfo = Placeholder.styling("bubble_info", bubble.formatInfo(proxy)) + val bubbleInfo = Placeholder.styling("bubble_info", bubble.formatInfo(userCache)) val renderedMessage = formatConfig.bubblePrefix.render(bubbleInfo).append(space()).append(formattedMessage) bubble.players.forEach { uuid -> proxy.playerOrNull(uuid)?.sendMessage(renderedMessage) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 4e4a5e5..0f38e86 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -23,6 +23,7 @@ fun PluginScope.createBubbleFeature( database: Storage, chatConfirmations: ChatConfirmations, formatConfig: FormatConfig, + userCache: UserCache, ): BubbleManager { val bubbleManager = BubbleManager() commandManager.apply { @@ -43,7 +44,17 @@ fun PluginScope.createBubbleFeature( commandCompletions.setDefaultCompletion("boolean", Boolean::class.java) commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java) } - registerCommands(BubbleCommand(messenger, proxy, database, bubbleManager, chatConfirmations, formatConfig)) + registerCommands( + BubbleCommand( + messenger, + proxy, + database, + bubbleManager, + chatConfirmations, + formatConfig, + userCache, + ) + ) return bubbleManager } @@ -56,6 +67,7 @@ private class BubbleCommand( private val bubbleManager: BubbleManager, private val chatConfirmations: ChatConfirmations, private val formatConfig: FormatConfig, + private val userCache: UserCache, ) : BaseCommand() { @CatchUnknown @@ -196,7 +208,7 @@ private class BubbleCommand( } sender.sendMessage( Component.textOfChildren( - Component.text(bubble.playersString(proxy)), + Component.text(bubble.playersString(userCache)), " | ".render(), info, ) @@ -272,12 +284,11 @@ class Bubble( val invitedPlayers: MutableSet, var isPrivate: Boolean, ) { - fun playersString(proxy: ProxyServer) = players - .mapNotNull { uuid -> proxy.playerOrNull(uuid)?.username } - .joinToString(", ") + fun playersString(userCache: UserCache) = + players.joinToString(", ", transform = userCache::usernameOrUuid) - fun formatInfo(proxy: ProxyServer) = - HoverEvent.showText(Component.text("Bubble: ${playersString(proxy)}")) + fun formatInfo(userCache: UserCache) = + HoverEvent.showText(Component.text("Bubble: ${playersString(userCache)}")) fun joinButton(proxy: ProxyServer): Component { val ownerName = proxy.playerOrNull(owner)?.username ?: owner.toString() From 4f44c9e7ca3b6f7897a3c086f51eeae9601b579f Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:51:37 +0300 Subject: [PATCH 41/63] Add bubble members to invite, cleanup --- chattore/src/main/kotlin/Messenger.kt | 4 +-- chattore/src/main/kotlin/feature/Bubble.kt | 30 ++++++++++++--------- chattore/src/main/kotlin/feature/Discord.kt | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 8f5ca41..8938bc4 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -101,13 +101,13 @@ class Messenger( "prefix" toC prefix, ) - val globalChatReceivers = proxy.all { it.uniqueId !in excludedFromGlobalChat } + val globalChat = proxy.all { it.uniqueId !in excludedFromGlobalChat } fun broadcastChatMessage(player: Player, message: String) { logger.info("${player.username} (${player.uniqueId}): $message") val originServer = player.currentServer.getOrNull()?.serverInfo?.name ?: "VOID" val compoPrefix = formatPrefix(player) - globalChatReceivers.sendMessage(formatChatMessage(message, player, prefix = compoPrefix)) + globalChat.sendMessage(formatChatMessage(message, player, prefix = compoPrefix)) val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix) val discordBroadcast = DiscordBroadcastEvent( diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 0f38e86..77f3d8c 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -9,9 +9,10 @@ import com.velocitypowered.api.command.CommandSource import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer import net.kyori.adventure.text.Component -import net.kyori.adventure.text.Component.space -import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.Component.* +import net.kyori.adventure.text.event.ClickEvent.runCommand import net.kyori.adventure.text.event.HoverEvent +import net.kyori.adventure.text.event.HoverEvent.showText import org.openredstone.chattore.* import java.util.* @@ -100,7 +101,12 @@ private class BubbleCommand( bubble.invitedPlayers.add(player.uniqueId) sender.sendInfo("Invited ${player.username}.") player.sendInfoC( - "${sender.username} invited you to their bubble. ".toComponent().append(bubble.joinButton(proxy)), + textOfChildren( + text("${sender.username} invited you to their bubble. "), + bubble.joinButton(userCache), + newline(), + text("Currently in the bubble: ${bubble.playersString(userCache)}"), + ), ) } @@ -204,11 +210,11 @@ private class BubbleCommand( val info = if (sender.uniqueId in bubble.players) { "Your current bubble".render() } else { - bubble.joinButton(proxy) + bubble.joinButton(userCache) } sender.sendMessage( - Component.textOfChildren( - Component.text(bubble.playersString(userCache)), + textOfChildren( + text(bubble.playersString(userCache)), " | ".render(), info, ) @@ -287,14 +293,14 @@ class Bubble( fun playersString(userCache: UserCache) = players.joinToString(", ", transform = userCache::usernameOrUuid) - fun formatInfo(userCache: UserCache) = - HoverEvent.showText(Component.text("Bubble: ${playersString(userCache)}")) + fun formatInfo(userCache: UserCache): HoverEvent = + showText(text("Bubble: ${playersString(userCache)}")) - fun joinButton(proxy: ProxyServer): Component { - val ownerName = proxy.playerOrNull(owner)?.username ?: owner.toString() + fun joinButton(userCache: UserCache): Component { + val ownerName = userCache.usernameOrUuid(owner) return "[Join]".render() - .clickEvent(ClickEvent.runCommand("/bubble join $ownerName")) - .hoverEvent(HoverEvent.showText(Component.text("Click to join bubble"))) + .clickEvent(runCommand("/bubble join $ownerName")) + .hoverEvent(showText(text("Click to join bubble"))) } } diff --git a/chattore/src/main/kotlin/feature/Discord.kt b/chattore/src/main/kotlin/feature/Discord.kt index 3b2dd5a..9cf4297 100644 --- a/chattore/src/main/kotlin/feature/Discord.kt +++ b/chattore/src/main/kotlin/feature/Discord.kt @@ -150,7 +150,7 @@ private class DiscordListener( val url = matchResult.groupValues[2].trim() "$text: $url" }.replace("""\s+""".toRegex(), " ") - messenger.globalChatReceivers.sendRichMessage( + messenger.globalChat.sendRichMessage( config.ingameFormat, "sender" toS displayName, "message" toC messenger.prepareChatMessage(transformedMessage, null), From 944119d27028d2f1e78fb81430e7f7e853aa1f73 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:00:50 +0300 Subject: [PATCH 42/63] Move some command completions into better locations --- chattore/src/main/kotlin/ChattORE.kt | 5 ++++- chattore/src/main/kotlin/feature/Bubble.kt | 3 --- chattore/src/main/kotlin/feature/Message.kt | 7 ++++--- chattore/src/main/kotlin/feature/Nickname.kt | 4 +++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 1b4be09..5741909 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -1,6 +1,7 @@ package org.openredstone.chattore import co.aikar.commands.* +import co.aikar.commands.velocity.contexts.OnlinePlayer import com.google.inject.Inject import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyInitializeEvent @@ -38,7 +39,9 @@ class ChattORE @Inject constructor( val pluginScope = PluginScope(this, ChattORE::class.java, proxy, dataFolder, logger, commandManager) commandManager.apply { setDefaultExceptionHandler(::handleCommandException, false) - commandCompletions.registerCompletion("username") { listOf(it.player.username) } + commandCompletions.registerStaticCompletion("boolean", arrayOf("true", "false")) + commandCompletions.setDefaultCompletion("boolean", Boolean::class.java) + commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java) @Suppress("DEPRECATION") enableUnstableAPI("help") } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 77f3d8c..0de3322 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -41,9 +41,6 @@ fun PluginScope.createBubbleFeature( } bubble } - commandCompletions.registerStaticCompletion("boolean", arrayOf("true", "false")) - commandCompletions.setDefaultCompletion("boolean", Boolean::class.java) - commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java) } registerCommands( BubbleCommand( diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 253f989..2ee6250 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -1,7 +1,10 @@ package org.openredstone.chattore.feature import co.aikar.commands.BaseCommand -import co.aikar.commands.annotation.* +import co.aikar.commands.annotation.CommandAlias +import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Default +import co.aikar.commands.annotation.Syntax import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer @@ -29,8 +32,6 @@ private class Message( ) : BaseCommand() { @Default @Syntax("[target] ") - // unsure if this is needed - @CommandCompletion("@players") fun default(sender: Player, recipient: OnlinePlayer, message: String) { sendMessage(logger, messenger, replyMap, sender, recipient.player, message) } diff --git a/chattore/src/main/kotlin/feature/Nickname.kt b/chattore/src/main/kotlin/feature/Nickname.kt index 3c6e5ae..88fad1c 100644 --- a/chattore/src/main/kotlin/feature/Nickname.kt +++ b/chattore/src/main/kotlin/feature/Nickname.kt @@ -57,12 +57,14 @@ fun PluginScope.createNicknameFeature( listOf() }) } + commandCompletions.registerCompletion(COMPLETION_SENDER_USERNAME) { listOfNotNull(it.player?.username) } } registerCommands(Nickname(database, proxy, userCache, config)) registerListeners(NicknameListener(database, userCache, config)) } private const val COMPLETION_COLORS = "colors" +private const val COMPLETION_SENDER_USERNAME = "senderUsername" val hexColorMap = mapOf( "0" to Pair("#000000", "black"), @@ -146,7 +148,7 @@ private class Nickname( @Subcommand("presets") @CommandPermission("chattore.nick.preset") - @CommandCompletion("@username") + @CommandCompletion("@${COMPLETION_SENDER_USERNAME}") fun presets(player: Player, @Optional shownText: String?) { val renderedPresets = ArrayList() for ((presetName, preset) in config.presets) { From f30c534d3ab4599596f08e4afa869e3ea826dc57 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:03:17 +0300 Subject: [PATCH 43/63] Add chat confirmations to DM --- chattore/src/main/kotlin/ChattORE.kt | 2 +- chattore/src/main/kotlin/feature/Message.kt | 23 +++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 5741909..6f131b8 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -60,7 +60,7 @@ class ChattORE @Inject constructor( createHelpOpFeature() createJoinLeaveFeature(config.format) createMailFeature(database, userCache) - createMessageFeature(messenger) + createMessageFeature(messenger, chatConfirmations) createNicknameFeature( database, userCache, NicknameConfig( config.clearNicknameOnChange, diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 2ee6250..f1e492a 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -15,25 +15,33 @@ import java.util.concurrent.ConcurrentHashMap fun PluginScope.createMessageFeature( messenger: Messenger, + chatConfirmations: ChatConfirmations, ) { val replyMap = ConcurrentHashMap() registerCommands( - Message(logger, messenger, replyMap), - Reply(proxy, logger, messenger, replyMap), + Message(proxy, logger, messenger, replyMap, chatConfirmations), + Reply(proxy, logger, messenger, replyMap, chatConfirmations), ) } @CommandAlias("m|pm|msg|message|vmsg|vmessage|whisper|tell") @CommandPermission("chattore.message") private class Message( + private val proxy: ProxyServer, private val logger: Logger, private val messenger: Messenger, private val replyMap: ConcurrentHashMap, + private val chatConfirmations: ChatConfirmations, ) : BaseCommand() { @Default @Syntax("[target] ") fun default(sender: Player, recipient: OnlinePlayer, message: String) { - sendMessage(logger, messenger, replyMap, sender, recipient.player, message) + val recipientUuid = recipient.player.uniqueId + chatConfirmations.submit(sender, message) { + val recipientPlayer = proxy.playerOrNull(recipientUuid) + ?: throw ChattoreException("The person you're trying to message is no longer online!") + sendMessage(logger, messenger, replyMap, sender, recipientPlayer, message) + } } } @@ -44,13 +52,16 @@ private class Reply( private val logger: Logger, private val messenger: Messenger, private val replyMap: ConcurrentHashMap, + private val chatConfirmations: ChatConfirmations, ) : BaseCommand() { @Default fun default(sender: Player, message: String) { val recipientUuid = replyMap[sender.uniqueId] ?: throw ChattoreException("You have no one to reply to!") - val recipient = proxy.playerOrNull(recipientUuid) - ?: throw ChattoreException("The person you are trying to reply to is no longer online!") - sendMessage(logger, messenger, replyMap, sender, recipient, message) + chatConfirmations.submit(sender, message) { + val recipient = proxy.playerOrNull(recipientUuid) + ?: throw ChattoreException("The person you are trying to reply to is no longer online!") + sendMessage(logger, messenger, replyMap, sender, recipient, message) + } } } From bcac58701461c01c7b0dca24f9c2d17bb42757dd Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:28:44 +0300 Subject: [PATCH 44/63] Extract chat confirmations into their own file --- chattore/src/main/kotlin/feature/Chat.kt | 60 ----------------- .../main/kotlin/feature/ChatConfirmations.kt | 66 +++++++++++++++++++ 2 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 chattore/src/main/kotlin/feature/ChatConfirmations.kt diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 2197785..948dfda 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -1,69 +1,9 @@ package org.openredstone.chattore.feature -import co.aikar.commands.BaseCommand -import co.aikar.commands.annotation.CommandAlias -import co.aikar.commands.annotation.CommandPermission -import co.aikar.commands.annotation.Default import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.PlayerChatEvent -import com.velocitypowered.api.proxy.Player import org.openredstone.chattore.* -import org.slf4j.Logger import java.util.* -import java.util.concurrent.ConcurrentHashMap - -data class ChatConfirmationConfig( - val regexes: List = listOf(), -) - -fun PluginScope.createChatConfirmations(config: ChatConfirmationConfig): ChatConfirmations = - ChatConfirmations(config, logger).also { registerCommands(it.ConfirmMessage()) } - -class ChatConfirmations( - config: ChatConfirmationConfig, - private val logger: Logger, -) { - private val regexes = config.regexes.mapNotNull { pattern -> - runCatching { Regex(pattern, RegexOption.IGNORE_CASE) } - .onFailure { logger.error("Invalid regex $pattern: ${it.message}") } - .getOrNull() - } - - private val flags = ConcurrentHashMap Unit>>() - fun submit(player: Player, message: String, proceed: () -> Unit) { - val matches = regexes.filter { it.containsMatchIn(message) } - if (matches.isEmpty()) { - flags.remove(player.uniqueId) - proceed() - return - } - fun String.highlight(r: Regex) = r.replace(this) { match -> "${match.value}" } - val highlighted = matches.fold(message, String::highlight) - logger.info("${player.username} (${player.uniqueId}) Attempting to send flagged message: $message") - player.sendSimpleMM( - "The following message was not sent because it contained " + - "potentially inappropriate language:To send this message anyway, run " + - "/confirmmessage.", - highlighted, - ) - flags[player.uniqueId] = message to proceed - return - } - - @CommandAlias("confirmmessage") - @CommandPermission("chattore.confirmmessage") - inner class ConfirmMessage : BaseCommand() { - @Default - fun default(player: Player) { - val (message, proceed) = flags[player.uniqueId] - ?: throw ChattoreException("You have no message to confirm!") - player.sendRichMessage("Override recognized") - flags.remove(player.uniqueId) - logger.info("${player.username} (${player.uniqueId}) FLAGGED MESSAGE OVERRIDE: $message") - proceed() - } - } -} fun PluginScope.createChatFeature( messenger: Messenger, diff --git a/chattore/src/main/kotlin/feature/ChatConfirmations.kt b/chattore/src/main/kotlin/feature/ChatConfirmations.kt new file mode 100644 index 0000000..5fdbe04 --- /dev/null +++ b/chattore/src/main/kotlin/feature/ChatConfirmations.kt @@ -0,0 +1,66 @@ +package org.openredstone.chattore.feature + +import co.aikar.commands.BaseCommand +import co.aikar.commands.annotation.CommandAlias +import co.aikar.commands.annotation.CommandPermission +import co.aikar.commands.annotation.Default +import com.velocitypowered.api.proxy.Player +import org.openredstone.chattore.ChattoreException +import org.openredstone.chattore.PluginScope +import org.openredstone.chattore.sendSimpleMM +import org.slf4j.Logger +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +data class ChatConfirmationConfig( + val regexes: List = listOf(), +) + +fun PluginScope.createChatConfirmations(config: ChatConfirmationConfig): ChatConfirmations = + ChatConfirmations(config, logger).also { registerCommands(it.ConfirmMessage()) } + +class ChatConfirmations( + config: ChatConfirmationConfig, + private val logger: Logger, +) { + private val regexes = config.regexes.mapNotNull { pattern -> + runCatching { Regex(pattern, RegexOption.IGNORE_CASE) } + .onFailure { logger.error("Invalid regex $pattern: ${it.message}") } + .getOrNull() + } + private val flags = ConcurrentHashMap Unit>>() + + fun submit(player: Player, message: String, proceed: () -> Unit) { + val matches = regexes.filter { it.containsMatchIn(message) } + if (matches.isEmpty()) { + flags.remove(player.uniqueId) + proceed() + return + } + fun String.highlight(r: Regex) = r.replace(this) { match -> "${match.value}" } + val highlighted = matches.fold(message, String::highlight) + logger.info("${player.username} (${player.uniqueId}) Attempting to send flagged message: $message") + player.sendSimpleMM( + "The following message was not sent because it contained " + + "potentially inappropriate language:To send this message anyway, run " + + "/confirmmessage.", + highlighted, + ) + flags[player.uniqueId] = message to proceed + return + } + + @CommandAlias("confirmmessage") + @CommandPermission("chattore.confirmmessage") + inner class ConfirmMessage : BaseCommand() { + @Default + fun default(player: Player) { + val (message, proceed) = flags[player.uniqueId] + ?: throw ChattoreException("You have no message to confirm!") + player.sendRichMessage("Override recognized") + flags.remove(player.uniqueId) + logger.info("${player.username} (${player.uniqueId}) FLAGGED MESSAGE OVERRIDE: $message") + proceed() + } + } +} From afd0950e5f92dca5c9de1de8a19499f0cf6e816e Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:06:22 +0300 Subject: [PATCH 45/63] Cache settings in memory to reduce database traffic --- chattore/src/main/kotlin/Storage.kt | 46 +++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/chattore/src/main/kotlin/Storage.kt b/chattore/src/main/kotlin/Storage.kt index 24c2b13..f9bb49e 100644 --- a/chattore/src/main/kotlin/Storage.kt +++ b/chattore/src/main/kotlin/Storage.kt @@ -1,6 +1,9 @@ package org.openredstone.chattore +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction @@ -45,7 +48,7 @@ object JsonSetting : Table("setting") { val uuidKeyIndex = index("setting_uuid_key_index", true, uuid, key) } -class Setting(val key: String) +class Setting(val key: String) class Storage( dbFile: Path, @@ -138,19 +141,38 @@ class Storage( } } - inline fun setSetting(setting: Setting, uuid: UUID, value: T) = transaction(database) { - JsonSetting.upsert { - it[JsonSetting.uuid] = uuid.toString() - it[key] = setting.key - it[JsonSetting.value] = Json.encodeToString(value) + private val settingCache = ConcurrentHashMap, UUID>, Any>() + + // these ugly wrappers are needed due to reified generics and to keep settingCache private + inline fun setSetting(setting: Setting, uuid: UUID, value: T) = + unsafeSetSetting(setting, uuid, value, Json.serializersModule.serializer()) + + fun unsafeSetSetting(setting: Setting, uuid: UUID, value: T, serializer: SerializationStrategy) { + transaction(database) { + JsonSetting.upsert { + it[JsonSetting.uuid] = uuid.toString() + it[key] = setting.key + it[JsonSetting.value] = Json.encodeToString(serializer, value) + } } + settingCache[setting to uuid] = value } - inline fun getSetting(setting: Setting, uuid: UUID): T? = transaction { - val result = JsonSetting.selectAll().where { - (JsonSetting.uuid eq uuid.toString()) and (JsonSetting.key eq setting.key) - }.singleOrNull() ?: return@transaction null - val jsonString = result[JsonSetting.value] - Json.decodeFromString(jsonString) + inline fun getSetting(setting: Setting, uuid: UUID): T? = + unsafeGetSetting(setting, uuid, Json.serializersModule.serializer()) + + fun unsafeGetSetting(setting: Setting, uuid: UUID, deserializer: DeserializationStrategy): T? { + val cached = settingCache[setting to uuid] + @Suppress("UNCHECKED_CAST") + if (cached != null) return cached as T + val value = transaction { + val result = JsonSetting.selectAll().where { + (JsonSetting.uuid eq uuid.toString()) and (JsonSetting.key eq setting.key) + }.singleOrNull() ?: return@transaction null + val jsonString = result[JsonSetting.value] + Json.decodeFromString(deserializer, jsonString) + } ?: return null + settingCache[setting to uuid] = value + return value } } From c8d5d1dfc9fe21b78c3b612f419739fbb3d4fe2e Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:46:08 +0300 Subject: [PATCH 46/63] Separate spying into command and social spy --- chattore/src/main/kotlin/feature/Spying.kt | 53 +++++++++++++--------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Spying.kt b/chattore/src/main/kotlin/feature/Spying.kt index 6e2abdc..b3423be 100644 --- a/chattore/src/main/kotlin/feature/Spying.kt +++ b/chattore/src/main/kotlin/feature/Spying.kt @@ -3,7 +3,6 @@ package org.openredstone.chattore.feature import co.aikar.commands.BaseCommand import co.aikar.commands.annotation.CommandAlias import co.aikar.commands.annotation.CommandPermission -import co.aikar.commands.annotation.Default import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.command.CommandExecuteEvent import com.velocitypowered.api.proxy.Player @@ -12,21 +11,23 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component.space import org.openredstone.chattore.* -private val SpyEnabled = Setting("spy") +// TODO: rename the key, requires a DB migration +private val CommandSpyEnabled = Setting("spy") +private val SocialSpyEnabled = Setting("socialSpy") typealias Wiretap = (Component) -> Unit -fun PluginScope.createSpyingFeature( - database: Storage, - formatConfig: FormatConfig, -): Wiretap { - // TODO: this hits the DB way WAY way too often xd - fun Player.isSpying() = database.getSetting(SpyEnabled, uniqueId) ?: false - val spies = proxy.all { it.hasChattorePrivilege && it.isSpying() } - registerCommands(CommandSpy(database)) - registerListeners(CommandListener(spies)) +fun PluginScope.createSpyingFeature(database: Storage, formatConfig: FormatConfig): Wiretap { + fun spyAudience(spySetting: Setting) = + proxy.all { it.hasChattorePrivilege && database.getSetting(spySetting, it.uniqueId) == true } + + val commandSpies = spyAudience(CommandSpyEnabled) + val socialSpies = spyAudience(SocialSpyEnabled) + registerCommands(SpyCommands(database)) + registerListeners(CommandListener(commandSpies)) + val spyPrefix = formatConfig.spyPrefix.render().append(space()) - return { secrets -> spies.sendMessage(spyPrefix.append(secrets)) } + return { secrets -> socialSpies.sendMessage(spyPrefix.append(secrets)) } } private class CommandListener( @@ -42,21 +43,29 @@ private class CommandListener( } } -@CommandAlias("commandspy") -@CommandPermission("chattore.commandspy") -private class CommandSpy( +private class SpyCommands( private val database: Storage, ) : BaseCommand() { - @Default - fun default(player: Player) { - val setting = database.getSetting(SpyEnabled, player.uniqueId) - val newSetting = !(setting ?: false) - database.setSetting(SpyEnabled, player.uniqueId, newSetting) + @CommandAlias("commandspy") + @CommandPermission("chattore.commandspy") + fun commandSpy(player: Player) { + toggleSpy(player, CommandSpyEnabled, "Command spy") + } + + @CommandAlias("socialspy") + @CommandPermission("chattore.socialspy") + fun socialSpy(player: Player) { + toggleSpy(player, SocialSpyEnabled, "Social spy") + } + + private fun toggleSpy(player: Player, spySetting: Setting, kind: String) { + val newSetting = database.getSetting(spySetting, player.uniqueId) != true + database.setSetting(spySetting, player.uniqueId, newSetting) player.sendInfo( if (newSetting) { - "You are now spying on commands." + "$kind enabled." } else { - "You are no longer spying on commands." + "$kind disabled." }, ) } From 8983b98b428df17df988b53c0a626e5b8474947a Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:51:21 +0300 Subject: [PATCH 47/63] Add /msg to social spy --- chattore/src/main/kotlin/ChattORE.kt | 2 +- chattore/src/main/kotlin/feature/Message.kt | 33 ++++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index 6f131b8..a899cb2 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -60,7 +60,7 @@ class ChattORE @Inject constructor( createHelpOpFeature() createJoinLeaveFeature(config.format) createMailFeature(database, userCache) - createMessageFeature(messenger, chatConfirmations) + createMessageFeature(messenger, chatConfirmations, wiretap) createNicknameFeature( database, userCache, NicknameConfig( config.clearNicknameOnChange, diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index f1e492a..1ae7a35 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -16,11 +16,12 @@ import java.util.concurrent.ConcurrentHashMap fun PluginScope.createMessageFeature( messenger: Messenger, chatConfirmations: ChatConfirmations, + wiretap: Wiretap, ) { val replyMap = ConcurrentHashMap() registerCommands( - Message(proxy, logger, messenger, replyMap, chatConfirmations), - Reply(proxy, logger, messenger, replyMap, chatConfirmations), + Message(proxy, logger, messenger, replyMap, chatConfirmations, wiretap), + Reply(proxy, logger, messenger, replyMap, chatConfirmations, wiretap), ) } @@ -32,6 +33,7 @@ private class Message( private val messenger: Messenger, private val replyMap: ConcurrentHashMap, private val chatConfirmations: ChatConfirmations, + private val wiretap: Wiretap, ) : BaseCommand() { @Default @Syntax("[target] ") @@ -40,7 +42,7 @@ private class Message( chatConfirmations.submit(sender, message) { val recipientPlayer = proxy.playerOrNull(recipientUuid) ?: throw ChattoreException("The person you're trying to message is no longer online!") - sendMessage(logger, messenger, replyMap, sender, recipientPlayer, message) + sendMessage(logger, wiretap, messenger, replyMap, sender, recipientPlayer, message) } } } @@ -53,6 +55,7 @@ private class Reply( private val messenger: Messenger, private val replyMap: ConcurrentHashMap, private val chatConfirmations: ChatConfirmations, + private val wiretap: Wiretap, ) : BaseCommand() { @Default fun default(sender: Player, message: String) { @@ -60,13 +63,14 @@ private class Reply( chatConfirmations.submit(sender, message) { val recipient = proxy.playerOrNull(recipientUuid) ?: throw ChattoreException("The person you are trying to reply to is no longer online!") - sendMessage(logger, messenger, replyMap, sender, recipient, message) + sendMessage(logger, wiretap, messenger, replyMap, sender, recipient, message) } } } private fun sendMessage( logger: Logger, + wiretap: Wiretap, messenger: Messenger, replyMap: MutableMap, sender: Player, @@ -77,16 +81,17 @@ private fun sendMessage( "${sender.username} (${sender.uniqueId}) -> " + "${recipient.username} (${recipient.uniqueId}): $message" ) - sender.sendRichMessage( - "[me -> ] ", - "message" toC messenger.prepareChatMessage(message, sender), - "recipient" toS recipient.username, - ) - recipient.sendRichMessage( - "[ -> me] ", - "message" toC messenger.prepareChatMessage(message, sender), - "sender" toS sender.username, - ) + + val preparedMessage = messenger.prepareChatMessage(message, sender) + fun renderDM(senderName: String, recipientName: String) = + "[ -> ] ".render( + "sender" toS senderName, "recipient" toS recipientName, "message" toC preparedMessage, + ) + + sender.sendMessage(renderDM("me", recipient.username)) + recipient.sendMessage(renderDM(sender.username, "me")) + wiretap(renderDM(sender.username, recipient.username)) + replyMap[recipient.uniqueId] = sender.uniqueId replyMap[sender.uniqueId] = recipient.uniqueId } From dbf4a7bcc07216d80efbb4be03be997ba3cbcf08 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:03:24 +0300 Subject: [PATCH 48/63] Compress Message.kt --- chattore/src/main/kotlin/feature/Message.kt | 114 ++++++++------------ 1 file changed, 44 insertions(+), 70 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 1ae7a35..57a5148 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -7,9 +7,7 @@ import co.aikar.commands.annotation.Default import co.aikar.commands.annotation.Syntax import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player -import com.velocitypowered.api.proxy.ProxyServer import org.openredstone.chattore.* -import org.slf4j.Logger import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -19,79 +17,55 @@ fun PluginScope.createMessageFeature( wiretap: Wiretap, ) { val replyMap = ConcurrentHashMap() - registerCommands( - Message(proxy, logger, messenger, replyMap, chatConfirmations, wiretap), - Reply(proxy, logger, messenger, replyMap, chatConfirmations, wiretap), - ) -} -@CommandAlias("m|pm|msg|message|vmsg|vmessage|whisper|tell") -@CommandPermission("chattore.message") -private class Message( - private val proxy: ProxyServer, - private val logger: Logger, - private val messenger: Messenger, - private val replyMap: ConcurrentHashMap, - private val chatConfirmations: ChatConfirmations, - private val wiretap: Wiretap, -) : BaseCommand() { - @Default - @Syntax("[target] ") - fun default(sender: Player, recipient: OnlinePlayer, message: String) { - val recipientUuid = recipient.player.uniqueId - chatConfirmations.submit(sender, message) { - val recipientPlayer = proxy.playerOrNull(recipientUuid) - ?: throw ChattoreException("The person you're trying to message is no longer online!") - sendMessage(logger, wiretap, messenger, replyMap, sender, recipientPlayer, message) - } + fun sendMessage(sender: Player, recipient: Player, message: String) { + logger.info( + "${sender.username} (${sender.uniqueId}) -> " + + "${recipient.username} (${recipient.uniqueId}): $message" + ) + + val preparedMessage = messenger.prepareChatMessage(message, sender) + fun renderDM(senderName: String, recipientName: String) = + "[ -> ] ".render( + "sender" toS senderName, "recipient" toS recipientName, "message" toC preparedMessage, + ) + + sender.sendMessage(renderDM("me", recipient.username)) + recipient.sendMessage(renderDM(sender.username, "me")) + wiretap(renderDM(sender.username, recipient.username)) + + replyMap[recipient.uniqueId] = sender.uniqueId + replyMap[sender.uniqueId] = recipient.uniqueId } -} -@CommandAlias("r|reply") -@CommandPermission("chattore.message") -private class Reply( - private val proxy: ProxyServer, - private val logger: Logger, - private val messenger: Messenger, - private val replyMap: ConcurrentHashMap, - private val chatConfirmations: ChatConfirmations, - private val wiretap: Wiretap, -) : BaseCommand() { - @Default - fun default(sender: Player, message: String) { - val recipientUuid = replyMap[sender.uniqueId] ?: throw ChattoreException("You have no one to reply to!") - chatConfirmations.submit(sender, message) { - val recipient = proxy.playerOrNull(recipientUuid) - ?: throw ChattoreException("The person you are trying to reply to is no longer online!") - sendMessage(logger, wiretap, messenger, replyMap, sender, recipient, message) + @CommandAlias("m|pm|msg|message|vmsg|vmessage|whisper|tell") + @CommandPermission("chattore.message") + class Message : BaseCommand() { + @Default + @Syntax("[target] ") + fun default(sender: Player, recipient: OnlinePlayer, message: String) { + val recipientUuid = recipient.player.uniqueId + chatConfirmations.submit(sender, message) { + val recipientPlayer = proxy.playerOrNull(recipientUuid) + ?: throw ChattoreException("The person you're trying to message is no longer online!") + sendMessage(sender, recipientPlayer, message) + } } } -} -private fun sendMessage( - logger: Logger, - wiretap: Wiretap, - messenger: Messenger, - replyMap: MutableMap, - sender: Player, - recipient: Player, - message: String, -) { - logger.info( - "${sender.username} (${sender.uniqueId}) -> " + - "${recipient.username} (${recipient.uniqueId}): $message" - ) - - val preparedMessage = messenger.prepareChatMessage(message, sender) - fun renderDM(senderName: String, recipientName: String) = - "[ -> ] ".render( - "sender" toS senderName, "recipient" toS recipientName, "message" toC preparedMessage, - ) - - sender.sendMessage(renderDM("me", recipient.username)) - recipient.sendMessage(renderDM(sender.username, "me")) - wiretap(renderDM(sender.username, recipient.username)) + @CommandAlias("r|reply") + @CommandPermission("chattore.message") + class Reply : BaseCommand() { + @Default + fun default(sender: Player, message: String) { + val recipientUuid = replyMap[sender.uniqueId] ?: throw ChattoreException("You have no one to reply to!") + chatConfirmations.submit(sender, message) { + val recipient = proxy.playerOrNull(recipientUuid) + ?: throw ChattoreException("The person you are trying to reply to is no longer online!") + sendMessage(sender, recipient, message) + } + } + } - replyMap[recipient.uniqueId] = sender.uniqueId - replyMap[sender.uniqueId] = recipient.uniqueId + registerCommands(Message(), Reply()) } From 523f287a8cbf4085d3c015fb1c28b55295c0b08d Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:20:35 +0300 Subject: [PATCH 49/63] Make BUBBLE_OWNED consistent in casing with other constants --- chattore/src/main/kotlin/feature/Bubble.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 0de3322..0c84a19 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -17,7 +17,7 @@ import org.openredstone.chattore.* import java.util.* private val ShowGlobalChatInBubble = Setting("showGlobalChatInBubble") -private const val BUBBLE_OWNED = "bubble-owned" +private const val BUBBLE_OWNED = "bubbleOwned" fun PluginScope.createBubbleFeature( messenger: Messenger, From 1e41f4d3c39f7d51de7bf654d3c894069dc17338 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:22:20 +0300 Subject: [PATCH 50/63] Do not show syntax when not in a bubble --- chattore/src/main/kotlin/feature/Bubble.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 0c84a19..baace00 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -30,13 +30,14 @@ fun PluginScope.createBubbleFeature( commandManager.apply { commandContexts.registerIssuerOnlyContext(Bubble::class.java) { ctx -> val sender = ctx.sender as? Player - ?: throw InvalidCommandArgument("This command can only be used by players!") + ?: throw InvalidCommandArgument("This command can only be used by players!", false) val bubble = bubbleManager.getBubbleByPlayer(sender) - ?: throw InvalidCommandArgument("You are not in a bubble!") + ?: throw InvalidCommandArgument("You are not in a bubble!", false) if (ctx.hasFlag(BUBBLE_OWNED) && bubble.owner != sender.uniqueId) { val ownerName = proxy.playerOrNull(bubble.owner)?.username ?: bubble.owner.toString() throw InvalidCommandArgument( "You must be the owner of the bubble to use this command. (Current owner: $ownerName)", + false, ) } bubble From 121ce6d4bff3dadc42853c8f744f5d7bfcb4291e Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:39:05 +0300 Subject: [PATCH 51/63] Replace Component#append with Component#textOfChildren Append causes some styles to get inherited where they aren't welcome. textOfChildren is the right way to concatenate components. --- chattore/src/main/kotlin/Messenger.kt | 3 ++- chattore/src/main/kotlin/feature/Bubble.kt | 7 +++++-- chattore/src/main/kotlin/feature/Message.kt | 1 + chattore/src/main/kotlin/feature/Spying.kt | 21 +++++++++------------ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 8938bc4..6f787f6 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -123,7 +123,8 @@ class Messenger( logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message") val formattedMessage = formatChatMessage(message, player) val bubbleInfo = Placeholder.styling("bubble_info", bubble.formatInfo(userCache)) - val renderedMessage = formatConfig.bubblePrefix.render(bubbleInfo).append(space()).append(formattedMessage) + val renderedMessage = + Component.textOfChildren(formatConfig.bubblePrefix.render(bubbleInfo), space(), formattedMessage) bubble.players.forEach { uuid -> proxy.playerOrNull(uuid)?.sendMessage(renderedMessage) } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index baace00..7e87d2f 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -255,8 +255,11 @@ private class BubbleCommand( messenger.broadcastChatMessage(sender, message) if (sender.uniqueId in messenger.excludedFromGlobalChat) { sender.sendMessage( - formatConfig.shoutPrefix.render().append(space()) - .append(messenger.formatChatMessage(message, sender)) + textOfChildren( + formatConfig.shoutPrefix.render(), + space(), + messenger.formatChatMessage(message, sender), + ) ) } } diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 57a5148..2ee2296 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -7,6 +7,7 @@ import co.aikar.commands.annotation.Default import co.aikar.commands.annotation.Syntax import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player +import net.kyori.adventure.text.Component import org.openredstone.chattore.* import java.util.* import java.util.concurrent.ConcurrentHashMap diff --git a/chattore/src/main/kotlin/feature/Spying.kt b/chattore/src/main/kotlin/feature/Spying.kt index b3423be..35fc190 100644 --- a/chattore/src/main/kotlin/feature/Spying.kt +++ b/chattore/src/main/kotlin/feature/Spying.kt @@ -9,6 +9,7 @@ import com.velocitypowered.api.proxy.Player import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component.space +import net.kyori.adventure.text.Component.textOfChildren import org.openredstone.chattore.* // TODO: rename the key, requires a DB migration @@ -26,13 +27,11 @@ fun PluginScope.createSpyingFeature(database: Storage, formatConfig: FormatConfi registerCommands(SpyCommands(database)) registerListeners(CommandListener(commandSpies)) - val spyPrefix = formatConfig.spyPrefix.render().append(space()) - return { secrets -> socialSpies.sendMessage(spyPrefix.append(secrets)) } + val spyPrefix = formatConfig.spyPrefix.render() + return { secrets -> socialSpies.sendMessage(textOfChildren(spyPrefix, space(), secrets)) } } -private class CommandListener( - private val spies: Audience, -) { +private class CommandListener(private val spies: Audience) { @Subscribe fun onCommandEvent(event: CommandExecuteEvent) { spies.sendRichMessage( @@ -43,19 +42,17 @@ private class CommandListener( } } -private class SpyCommands( - private val database: Storage, -) : BaseCommand() { +private class SpyCommands(private val database: Storage) : BaseCommand() { @CommandAlias("commandspy") @CommandPermission("chattore.commandspy") fun commandSpy(player: Player) { - toggleSpy(player, CommandSpyEnabled, "Command spy") + toggleSpy(player, CommandSpyEnabled, "Command") } @CommandAlias("socialspy") @CommandPermission("chattore.socialspy") fun socialSpy(player: Player) { - toggleSpy(player, SocialSpyEnabled, "Social spy") + toggleSpy(player, SocialSpyEnabled, "Social") } private fun toggleSpy(player: Player, spySetting: Setting, kind: String) { @@ -63,9 +60,9 @@ private class SpyCommands( database.setSetting(spySetting, player.uniqueId, newSetting) player.sendInfo( if (newSetting) { - "$kind enabled." + "$kind spy enabled." } else { - "$kind disabled." + "$kind spy disabled." }, ) } From 4db62f3f08a04b6bb45f1efc54c3b65fb1ffe2c6 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:46:20 +0300 Subject: [PATCH 52/63] Bubble create and invite take list of players to invite --- chattore/src/main/kotlin/feature/Bubble.kt | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 7e87d2f..5b0efc3 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -15,6 +15,7 @@ import net.kyori.adventure.text.event.HoverEvent import net.kyori.adventure.text.event.HoverEvent.showText import org.openredstone.chattore.* import java.util.* +import kotlin.jvm.optionals.getOrNull private val ShowGlobalChatInBubble = Setting("showGlobalChatInBubble") private const val BUBBLE_OWNED = "bubbleOwned" @@ -42,6 +43,19 @@ fun PluginScope.createBubbleFeature( } bubble } + commandContexts.registerContext(Array::class.java) { ctx -> + // distinct() is called here already so that sendError is not called more than once per string. + // If this is upgraded to do fuzzy matching, then something different is needed. + ctx.args.map(String::lowercase).distinct() + .mapNotNull { arg -> + proxy.getPlayer(arg).getOrNull() ?: run { + ctx.sender.sendError("Player $arg is not online!") + null + } + } + .toTypedArray() + } + commandCompletions.setDefaultCompletion("players", Array::class.java) } registerCommands( BubbleCommand( @@ -77,20 +91,41 @@ private class BubbleCommand( help.showHelp() } + // NOTE: for some reason Array still requires the explicit CommandCompletion annotation @Subcommand("create|blow") @Description("Create (\"blow\") a bubble") - fun create(sender: Player) { + @CommandCompletion("@players") + fun create(sender: Player, @ConsumesRest players: Array) { if (bubbleManager.getBubbleByPlayer(sender) != null) throw ChattoreException("You are already in a bubble!") - bubbleManager.createBubble(sender.uniqueId) + val bubble = bubbleManager.createBubble(sender.uniqueId) addExcluded(sender.uniqueId) sender.sendInfo("Bubble created.") + sendInvites(sender, bubble, players) } @Subcommand("invite") - @Description("Invite a player to your bubble (if it is private)") - fun invite(sender: Player, bubble: Bubble, target: OnlinePlayer) { - val player = target.player + @Description("Invite players to your bubble (if it is private)") + @CommandCompletion("@players") + fun invite(sender: Player, bubble: Bubble, @ConsumesRest targets: Array) { + if (targets.isEmpty()) + throw ChattoreException("Please specify one or more players to invite.") + sendInvites(sender, bubble, targets) + } + + private fun sendInvites(sender: Player, bubble: Bubble, targets: Array) { + for (player in targets) { + try { + doInvite(sender, bubble, player) + } catch (e: ChattoreException) { + sender.sendError(e.message ?: throw e) + } + } + } + + private fun doInvite(sender: Player, bubble: Bubble, player: Player) { + if (sender == player) + throw ChattoreException("You cannot invite yourself!") if (player.uniqueId in bubble.invitedPlayers) throw ChattoreException("${player.username} is already invited!") if (player.uniqueId in bubble.players) @@ -309,8 +344,10 @@ class BubbleManager { private val _bubbles: MutableList = mutableListOf() val bubbles: List get() = _bubbles - fun createBubble(player: UUID) { - _bubbles.add(Bubble(player, mutableSetOf(player), mutableSetOf(), false)) + fun createBubble(player: UUID): Bubble { + val bubble = Bubble(player, mutableSetOf(player), mutableSetOf(), false) + _bubbles.add(bubble) + return bubble } fun removeBubble(bubble: Bubble) { From 658343242e53951e323eea11ce6d34751b171d7f Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:55:58 +0300 Subject: [PATCH 53/63] Refuse invites to public bubbles --- chattore/src/main/kotlin/feature/Bubble.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 5b0efc3..2ad1897 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -108,6 +108,8 @@ private class BubbleCommand( @Description("Invite players to your bubble (if it is private)") @CommandCompletion("@players") fun invite(sender: Player, bubble: Bubble, @ConsumesRest targets: Array) { + if (!bubble.isPrivate) + throw ChattoreException("Your bubble is public, anyone can join without invitation.") if (targets.isEmpty()) throw ChattoreException("Please specify one or more players to invite.") sendInvites(sender, bubble, targets) From fd19901e07d0b2f5ebf563566251c1ef9b2efa23 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:56:36 +0300 Subject: [PATCH 54/63] Small refactor --- chattore/src/main/kotlin/feature/Bubble.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 2ad1897..cccc28b 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -346,11 +346,8 @@ class BubbleManager { private val _bubbles: MutableList = mutableListOf() val bubbles: List get() = _bubbles - fun createBubble(player: UUID): Bubble { - val bubble = Bubble(player, mutableSetOf(player), mutableSetOf(), false) - _bubbles.add(bubble) - return bubble - } + fun createBubble(player: UUID): Bubble = + Bubble(player, mutableSetOf(player), mutableSetOf(), isPrivate = true).also(_bubbles::add) fun removeBubble(bubble: Bubble) { _bubbles.remove(bubble) From 0c9a6a8b78e6ba474b80de0d1b913f555f35394f Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:03:21 +0300 Subject: [PATCH 55/63] Move setting default values into Setting class --- chattore/src/main/kotlin/Storage.kt | 8 ++++---- chattore/src/main/kotlin/feature/Bubble.kt | 4 ++-- chattore/src/main/kotlin/feature/Spying.kt | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/chattore/src/main/kotlin/Storage.kt b/chattore/src/main/kotlin/Storage.kt index f9bb49e..2a1890a 100644 --- a/chattore/src/main/kotlin/Storage.kt +++ b/chattore/src/main/kotlin/Storage.kt @@ -48,7 +48,7 @@ object JsonSetting : Table("setting") { val uuidKeyIndex = index("setting_uuid_key_index", true, uuid, key) } -class Setting(val key: String) +class Setting(val key: String, val default: T) class Storage( dbFile: Path, @@ -158,10 +158,10 @@ class Storage( settingCache[setting to uuid] = value } - inline fun getSetting(setting: Setting, uuid: UUID): T? = + inline fun getSetting(setting: Setting, uuid: UUID): T = unsafeGetSetting(setting, uuid, Json.serializersModule.serializer()) - fun unsafeGetSetting(setting: Setting, uuid: UUID, deserializer: DeserializationStrategy): T? { + fun unsafeGetSetting(setting: Setting, uuid: UUID, deserializer: DeserializationStrategy): T { val cached = settingCache[setting to uuid] @Suppress("UNCHECKED_CAST") if (cached != null) return cached as T @@ -171,7 +171,7 @@ class Storage( }.singleOrNull() ?: return@transaction null val jsonString = result[JsonSetting.value] Json.decodeFromString(deserializer, jsonString) - } ?: return null + } ?: setting.default settingCache[setting to uuid] = value return value } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index cccc28b..0d573a8 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -17,7 +17,7 @@ import org.openredstone.chattore.* import java.util.* import kotlin.jvm.optionals.getOrNull -private val ShowGlobalChatInBubble = Setting("showGlobalChatInBubble") +private val ShowGlobalChatInBubble = Setting("showGlobalChatInBubble", default = false) private const val BUBBLE_OWNED = "bubbleOwned" fun PluginScope.createBubbleFeature( @@ -316,7 +316,7 @@ private class BubbleCommand( } private fun addExcluded(uuid: UUID) { - if (database.getSetting(ShowGlobalChatInBubble, uuid) != true) { + if (!database.getSetting(ShowGlobalChatInBubble, uuid)) { messenger.excludedFromGlobalChat.add(uuid) } } diff --git a/chattore/src/main/kotlin/feature/Spying.kt b/chattore/src/main/kotlin/feature/Spying.kt index 35fc190..cea9a73 100644 --- a/chattore/src/main/kotlin/feature/Spying.kt +++ b/chattore/src/main/kotlin/feature/Spying.kt @@ -13,14 +13,14 @@ import net.kyori.adventure.text.Component.textOfChildren import org.openredstone.chattore.* // TODO: rename the key, requires a DB migration -private val CommandSpyEnabled = Setting("spy") -private val SocialSpyEnabled = Setting("socialSpy") +private val CommandSpyEnabled = Setting("spy", default = false) +private val SocialSpyEnabled = Setting("socialSpyEnabled", default = false) typealias Wiretap = (Component) -> Unit fun PluginScope.createSpyingFeature(database: Storage, formatConfig: FormatConfig): Wiretap { - fun spyAudience(spySetting: Setting) = - proxy.all { it.hasChattorePrivilege && database.getSetting(spySetting, it.uniqueId) == true } + fun spyAudience(spyEnabled: Setting) = + proxy.all { it.hasChattorePrivilege && database.getSetting(spyEnabled, it.uniqueId) } val commandSpies = spyAudience(CommandSpyEnabled) val socialSpies = spyAudience(SocialSpyEnabled) @@ -55,9 +55,9 @@ private class SpyCommands(private val database: Storage) : BaseCommand() { toggleSpy(player, SocialSpyEnabled, "Social") } - private fun toggleSpy(player: Player, spySetting: Setting, kind: String) { - val newSetting = database.getSetting(spySetting, player.uniqueId) != true - database.setSetting(spySetting, player.uniqueId, newSetting) + private fun toggleSpy(player: Player, spyEnabled: Setting, kind: String) { + val newSetting = !database.getSetting(spyEnabled, player.uniqueId) + database.setSetting(spyEnabled, player.uniqueId, newSetting) player.sendInfo( if (newSetting) { "$kind spy enabled." From 756ff8b5e6bf09213b1aa9e640a428727ad88b4d Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:04:54 +0300 Subject: [PATCH 56/63] Update Bubble command descriptions and README --- README.md | 26 +++++++++++----------- chattore/src/main/kotlin/feature/Bubble.kt | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 80f2bd5..ec1c594 100644 --- a/README.md +++ b/README.md @@ -43,19 +43,19 @@ Because we want to have a chat system that actually wOREks for us. ## Bubble Commands -| Command | Permission | Description | Aliases | -|------------------------------------|--------------------------|-----------------------------------------------------------|------------------------| -| `/bubble create` | `chattore.bubble` | Create ("blow") a bubble | `/bb create\|/bb blow` | -| `/bubble invite ` | `chattore.bubble` | Invite a player to your bubble (if it is private) | `/bb invite` | -| `/bubble join ` | `chattore.bubble` | Join a player's bubble | `/bb join` | -| `/bubble leave` | `chattore.bubble` | Leave your current bubble | `/bb leave` | -| `/bubble delete` | `chattore.bubble` | Delete ("pop") your own bubble | `/bb delete\|/bb pop` | -| `/bubble kick ` | `chattore.bubble` | Kick a player from your own bubble | `/bb kick` | -| `/bubble setprivate ` | `chattore.bubble` | Set the visibility of your own bubble | `/bb setprivate` | -| `/bubble list` | `chattore.bubble` | List all bubbles | `/bb list` | -| `/shout ` | `chattore.bubble` | Send a message to global chat when inside a bubble | No aliases | -| `/bubble burst ` | `chattore.bubble.manage` | Burst (delete) someone's bubble | `/bb burst` | -| `/bubble showglobalchat ` | `chattore.bubble` | Toggle the visibility of global chat when inside a bubble | `/bb sgc` | +| Command | Permission | Description | Aliases | +|------------------------------------|--------------------------|--------------------------------------------------------|------------------------| +| `/bubble create [players]` | `chattore.bubble` | Create ("blow") a bubble and invite players | `/bb create\|/bb blow` | +| `/bubble invite ` | `chattore.bubble` | Invite players to your bubble (if it is private) | `/bb invite` | +| `/bubble join ` | `chattore.bubble` | Join a player's bubble | `/bb join` | +| `/bubble leave` | `chattore.bubble` | Leave your current bubble | `/bb leave` | +| `/bubble delete` | `chattore.bubble` | Delete ("pop") your own bubble | `/bb delete\|/bb pop` | +| `/bubble kick ` | `chattore.bubble` | Kick a player from your own bubble | `/bb kick` | +| `/bubble setprivate ` | `chattore.bubble` | Set the visibility of your own bubble | `/bb setprivate` | +| `/bubble list` | `chattore.bubble` | List all bubbles | `/bb list` | +| `/shout ` | `chattore.bubble` | Send a message to global chat when in a bubble | No aliases | +| `/bubble burst ` | `chattore.bubble.manage` | Burst (delete) someone's bubble | `/bb burst` | +| `/bubble showglobalchat ` | `chattore.bubble` | Control the visibility of global chat when in a bubble | `/bb sgc` | ## Other Commands diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 0d573a8..3027de7 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -93,7 +93,7 @@ private class BubbleCommand( // NOTE: for some reason Array still requires the explicit CommandCompletion annotation @Subcommand("create|blow") - @Description("Create (\"blow\") a bubble") + @Description("Create (\"blow\") a bubble and invite players") @CommandCompletion("@players") fun create(sender: Player, @ConsumesRest players: Array) { if (bubbleManager.getBubbleByPlayer(sender) != null) From 09f8703451e9951c94c45c8ebb86411300c116ff Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:11:07 +0300 Subject: [PATCH 57/63] Optimize imports --- chattore/src/main/kotlin/feature/Chat.kt | 5 +++-- chattore/src/main/kotlin/feature/Message.kt | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index 948dfda..f83cfca 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -2,8 +2,9 @@ package org.openredstone.chattore.feature import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.player.PlayerChatEvent -import org.openredstone.chattore.* -import java.util.* +import org.openredstone.chattore.Messenger +import org.openredstone.chattore.PluginScope +import org.openredstone.chattore.sendError fun PluginScope.createChatFeature( messenger: Messenger, diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 2ee2296..57a5148 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -7,7 +7,6 @@ import co.aikar.commands.annotation.Default import co.aikar.commands.annotation.Syntax import co.aikar.commands.velocity.contexts.OnlinePlayer import com.velocitypowered.api.proxy.Player -import net.kyori.adventure.text.Component import org.openredstone.chattore.* import java.util.* import java.util.concurrent.ConcurrentHashMap From 222c5c4deb1235e1dda54b7596a90163f52b99db Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:20:12 +0300 Subject: [PATCH 58/63] Enforce more trailing commas --- .idea/codeStyles/Project.xml | 4 ++++ chattore/src/main/kotlin/ChattORE.kt | 7 ++++--- chattore/src/main/kotlin/Messenger.kt | 2 +- chattore/src/main/kotlin/Storage.kt | 4 ++-- chattore/src/main/kotlin/feature/Bubble.kt | 8 ++++---- chattore/src/main/kotlin/feature/Message.kt | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index bd93f4e..b59b03b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -2,6 +2,10 @@ diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index a899cb2..b875f02 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -23,7 +23,7 @@ import kotlin.io.path.exists url = "https://openredstone.org", description = "Because we want to have a chat system that actually wOREks for us.", authors = ["Nickster258", "PaukkuPalikka", "StackDoubleFlow", "sodiboo", "Waffle [Wueffi]"], - dependencies = [Dependency(id = "luckperms")] + dependencies = [Dependency(id = "luckperms")], ) class ChattORE @Inject constructor( private val proxy: ProxyServer, @@ -62,11 +62,12 @@ class ChattORE @Inject constructor( createMailFeature(database, userCache) createMessageFeature(messenger, chatConfirmations, wiretap) createNicknameFeature( - database, userCache, NicknameConfig( + database, userCache, + NicknameConfig( config.clearNicknameOnChange, // IDK, this when config config.nicknamePresets.mapValues { (_, v) -> NickPreset(v) }.toSortedMap(), - ) + ), ) createProfileFeature(database, luckPerms, userCache) } diff --git a/chattore/src/main/kotlin/Messenger.kt b/chattore/src/main/kotlin/Messenger.kt index 6f787f6..44efb2d 100644 --- a/chattore/src/main/kotlin/Messenger.kt +++ b/chattore/src/main/kotlin/Messenger.kt @@ -114,7 +114,7 @@ class Messenger( plainPrefix, player.username, originServer, - message + message, ) proxy.eventManager.fireAndForget(discordBroadcast) } diff --git a/chattore/src/main/kotlin/Storage.kt b/chattore/src/main/kotlin/Storage.kt index 2a1890a..e8ba1d5 100644 --- a/chattore/src/main/kotlin/Storage.kt +++ b/chattore/src/main/kotlin/Storage.kt @@ -64,7 +64,7 @@ class Storage( private fun initTables() = transaction(database) { SchemaUtils.create( - About, Mail, Nick, UsernameCache, JsonSetting + About, Mail, Nick, UsernameCache, JsonSetting, ) } @@ -130,7 +130,7 @@ class Storage( it[Mail.id], it[Mail.timestamp], UUID.fromString(it[Mail.sender]), - it[Mail.read] + it[Mail.read], ) } } diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 3027de7..7bbd7af 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -66,7 +66,7 @@ fun PluginScope.createBubbleFeature( chatConfirmations, formatConfig, userCache, - ) + ), ) return bubbleManager } @@ -252,7 +252,7 @@ private class BubbleCommand( text(bubble.playersString(userCache)), " | ".render(), info, - ) + ), ) } } @@ -281,7 +281,7 @@ private class BubbleCommand( } sender.sendInfo( if (showGlobalChat) "You will now see global chat in bubbles." - else "You will no longer see global chat in bubbles." + else "You will no longer see global chat in bubbles.", ) } @@ -296,7 +296,7 @@ private class BubbleCommand( formatConfig.shoutPrefix.render(), space(), messenger.formatChatMessage(message, sender), - ) + ), ) } } diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 57a5148..72eb425 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -21,7 +21,7 @@ fun PluginScope.createMessageFeature( fun sendMessage(sender: Player, recipient: Player, message: String) { logger.info( "${sender.username} (${sender.uniqueId}) -> " + - "${recipient.username} (${recipient.uniqueId}): $message" + "${recipient.username} (${recipient.uniqueId}): $message", ) val preparedMessage = messenger.prepareChatMessage(message, sender) From 984d0b58dab2e5f59e4462ce1ee142f13d0a8be8 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:20:21 +0300 Subject: [PATCH 59/63] Add .kotlin to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ddf6174..539acf9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea/* !.idea/codeStyles/ build/ +.kotlin/ From 722a029414fa6885b5703b05c3b4c12552f7a3a9 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:49:35 +0300 Subject: [PATCH 60/63] Add chat confirmations to funcommands, helpop, mail and player profile --- chattore/src/main/kotlin/ChattORE.kt | 8 ++-- .../src/main/kotlin/feature/Funcommands.kt | 20 ++++++---- chattore/src/main/kotlin/feature/HelpOp.kt | 21 +++++----- chattore/src/main/kotlin/feature/Mail.kt | 39 +++++++++++-------- chattore/src/main/kotlin/feature/Profile.kt | 10 +++-- 5 files changed, 58 insertions(+), 40 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index b875f02..fc00053 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -56,10 +56,10 @@ class ChattORE @Inject constructor( createChatFeature(messenger, chatConfirmations, bubbleManager) createChattoreFeature() createDiscordFeature(messenger, emojis, config.discord) - createFunCommandsFeature() - createHelpOpFeature() + createFunCommandsFeature(chatConfirmations) + createHelpOpFeature(chatConfirmations) createJoinLeaveFeature(config.format) - createMailFeature(database, userCache) + createMailFeature(database, userCache, chatConfirmations) createMessageFeature(messenger, chatConfirmations, wiretap) createNicknameFeature( database, userCache, @@ -69,7 +69,7 @@ class ChattORE @Inject constructor( config.nicknamePresets.mapValues { (_, v) -> NickPreset(v) }.toSortedMap(), ), ) - createProfileFeature(database, luckPerms, userCache) + createProfileFeature(database, luckPerms, userCache, chatConfirmations) } } diff --git a/chattore/src/main/kotlin/feature/Funcommands.kt b/chattore/src/main/kotlin/feature/Funcommands.kt index 67a5d1b..325f4fc 100644 --- a/chattore/src/main/kotlin/feature/Funcommands.kt +++ b/chattore/src/main/kotlin/feature/Funcommands.kt @@ -16,9 +16,9 @@ import net.kyori.adventure.text.event.HoverEvent.showText import org.openredstone.chattore.* import org.slf4j.Logger -fun PluginScope.createFunCommandsFeature() { +fun PluginScope.createFunCommandsFeature(chatConfirmations: ChatConfirmations) { val commands = Json.decodeFromString>(loadDataResourceAsString("commands.json")) - createFunCommands(logger, proxy, proxy.commandManager, commands) + createFunCommands(logger, proxy, chatConfirmations, proxy.commandManager, commands) registerCommands(FunCommandsCommand(commands)) } @@ -92,6 +92,7 @@ private data class FunCommand( private fun createFunCommands( logger: Logger, proxy: ProxyServer, + chatConfirmations: ChatConfirmations, commandManager: CommandManager, commands: List, ) { @@ -117,17 +118,20 @@ private fun createFunCommands( return@SimpleCommand } + val allArgs = args.joinToString(" ") val replacements = arrayOf( "name" toS source.username, - "arg-all" toS args.joinToString(" "), + "arg-all" toS allArgs, "arg-1" toS (args.getOrNull(1) ?: ""), - "arg-2" toS (args.getOrNull(2) ?: "") + "arg-2" toS (args.getOrNull(2) ?: ""), ) - cmd.globalChat?.let { proxy.all.sendRichMessage(it, *replacements) } - cmd.localChat?.let { source.sendRichMessage(it, *replacements) } - cmd.othersChat?.let { proxy.allBut(source).sendRichMessage(it, *replacements) } - cmd.run?.let { executeAction(it, source) } + chatConfirmations.submit(source, "/${invocation.alias()} $allArgs") { + cmd.globalChat?.let { proxy.all.sendRichMessage(it, *replacements) } + cmd.localChat?.let { source.sendRichMessage(it, *replacements) } + cmd.othersChat?.let { proxy.allBut(source).sendRichMessage(it, *replacements) } + cmd.run?.let { executeAction(it, source) } + } } commands.forEach { commandConfig -> diff --git a/chattore/src/main/kotlin/feature/HelpOp.kt b/chattore/src/main/kotlin/feature/HelpOp.kt index 6fdeb7f..5ce28a3 100644 --- a/chattore/src/main/kotlin/feature/HelpOp.kt +++ b/chattore/src/main/kotlin/feature/HelpOp.kt @@ -10,8 +10,8 @@ import com.velocitypowered.api.proxy.ProxyServer import org.openredstone.chattore.* import org.slf4j.Logger -fun PluginScope.createHelpOpFeature() { - registerCommands(HelpOp(logger, proxy)) +fun PluginScope.createHelpOpFeature(chatConfirmations: ChatConfirmations) { + registerCommands(HelpOp(logger, proxy, chatConfirmations)) } @CommandAlias("helpop|ac") @@ -19,17 +19,20 @@ fun PluginScope.createHelpOpFeature() { private class HelpOp( private val logger: Logger, private val proxy: ProxyServer, + private val chatConfirmations: ChatConfirmations, ) : BaseCommand() { @Default @Syntax("[message]") fun default(player: Player, statement: String) { if (statement.isEmpty()) throw ChattoreException("You have to have a problem first!") // : ) - logger.info("[HelpOp] ${player.username}: $statement") - proxy.all { it.hasChattorePrivilege || it.uniqueId == player.uniqueId } - .sendRichMessage( - "[Help] : ", - "message" toS statement, - "sender" toS player.username, - ) + chatConfirmations.submit(player, statement) { + logger.info("[HelpOp] ${player.username}: $statement") + proxy.all { it.hasChattorePrivilege || it.uniqueId == player.uniqueId } + .sendRichMessage( + "[Help] : ", + "message" toS statement, + "sender" toS player.username, + ) + } } } diff --git a/chattore/src/main/kotlin/feature/Mail.kt b/chattore/src/main/kotlin/feature/Mail.kt index 32bca5e..f9d3c40 100644 --- a/chattore/src/main/kotlin/feature/Mail.kt +++ b/chattore/src/main/kotlin/feature/Mail.kt @@ -19,8 +19,9 @@ import kotlin.math.min fun PluginScope.createMailFeature( database: Storage, userCache: UserCache, + chatConfirmations: ChatConfirmations, ) { - registerCommands(Mail(database, userCache)) + registerCommands(Mail(database, userCache, chatConfirmations)) registerListeners(MailListener(plugin, database, proxy)) } @@ -50,7 +51,7 @@ private class MailContainer(private val userCache: UserCache, private val messag private val pageSize = 6 fun getPage(page: Int = 0): Component { val maxPage = messages.size / pageSize - if (page > maxPage || page < 0) { + if (page !in 0..maxPage) { return "Invalid page requested".toComponent() } val pageStart = page * pageSize @@ -97,6 +98,7 @@ private class MailContainer(private val userCache: UserCache, private val messag private class Mail( private val database: Storage, private val userCache: UserCache, + private val chatConfirmations: ChatConfirmations, ) : BaseCommand() { private val mailTimeouts = mutableMapOf() @@ -107,7 +109,7 @@ private class Mail( fun mailbox(player: Player, @Default("0") page: Int) { val container = MailContainer( userCache, - database.getMessages(player.uniqueId) + database.getMessages(player.uniqueId), ) player.sendMessage(container.getPage(page)) } @@ -122,13 +124,15 @@ private class Mail( } val targetUuid = userCache.uuidOrNull(target) ?: throw ChattoreException("We do not recognize that user!") - mailTimeouts[player.uniqueId] = now - database.insertMessage(player.uniqueId, targetUuid, message) - player.sendRichMessage( - "[To ] ", - "message" toS message, - "recipient" toS target, - ) + chatConfirmations.submit(player, message) { + mailTimeouts[player.uniqueId] = now + database.insertMessage(player.uniqueId, targetUuid, message) + player.sendRichMessage( + "[To ] ", + "message" toS message, + "recipient" toS target, + ) + } } @Subcommand("read") @@ -152,11 +156,14 @@ private class MailListener( fun joinEvent(event: LoginEvent) { val unreadCount = database.getMessages(event.player.uniqueId).filter { !it.read }.size if (unreadCount > 0) - proxy.scheduler.buildTask(plugin, Runnable { - event.player.sendRichMessage( - "You have unread message(s)! Click here to view.", - "count" toS unreadCount.toString(), - ) - }).delay(2L, TimeUnit.SECONDS).schedule() + proxy.scheduler.buildTask( + plugin, + Runnable { + event.player.sendRichMessage( + "You have unread message(s)! Click here to view.", + "count" toS unreadCount.toString(), + ) + }, + ).delay(2L, TimeUnit.SECONDS).schedule() } } diff --git a/chattore/src/main/kotlin/feature/Profile.kt b/chattore/src/main/kotlin/feature/Profile.kt index 51ae306..980300a 100644 --- a/chattore/src/main/kotlin/feature/Profile.kt +++ b/chattore/src/main/kotlin/feature/Profile.kt @@ -16,8 +16,9 @@ fun PluginScope.createProfileFeature( database: Storage, luckPerm: LuckPerms, userCache: UserCache, + chatConfirmations: ChatConfirmations, ) { - registerCommands(Profile(proxy, database, luckPerm, userCache)) + registerCommands(Profile(proxy, database, luckPerm, userCache, chatConfirmations)) } @CommandAlias("profile|playerprofile") @@ -27,6 +28,7 @@ private class Profile( private val database: Storage, private val luckPerms: LuckPerms, private val userCache: UserCache, + private val chatConfirmations: ChatConfirmations, ) : BaseCommand() { @Subcommand("info") @@ -41,8 +43,10 @@ private class Profile( @Subcommand("about") @CommandPermission("chattore.profile.about") fun about(player: Player, about: String) { - database.setAbout(player.uniqueId, about) - player.sendInfo("Set your about to '$about'.") + chatConfirmations.submit(player, "/$execCommandLabel about $about") { + database.setAbout(player.uniqueId, about) + player.sendInfo("Set your about to '$about'.") + } } @Subcommand("setabout") From b5ad8c2c309068667e1c453a6255d3f7309b7021 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:30:12 +0300 Subject: [PATCH 61/63] Fix chat confirmations having stale Player objects and add socialspy to mail --- chattore/src/main/kotlin/ChattORE.kt | 2 +- chattore/src/main/kotlin/feature/Bubble.kt | 2 +- chattore/src/main/kotlin/feature/Chat.kt | 4 ++-- .../src/main/kotlin/feature/ChatConfirmations.kt | 16 ++++++++++++---- chattore/src/main/kotlin/feature/Funcommands.kt | 2 +- chattore/src/main/kotlin/feature/HelpOp.kt | 2 +- chattore/src/main/kotlin/feature/Mail.kt | 10 ++++++++-- chattore/src/main/kotlin/feature/Message.kt | 4 ++-- chattore/src/main/kotlin/feature/Profile.kt | 2 +- 9 files changed, 29 insertions(+), 15 deletions(-) diff --git a/chattore/src/main/kotlin/ChattORE.kt b/chattore/src/main/kotlin/ChattORE.kt index fc00053..25acc50 100644 --- a/chattore/src/main/kotlin/ChattORE.kt +++ b/chattore/src/main/kotlin/ChattORE.kt @@ -59,7 +59,7 @@ class ChattORE @Inject constructor( createFunCommandsFeature(chatConfirmations) createHelpOpFeature(chatConfirmations) createJoinLeaveFeature(config.format) - createMailFeature(database, userCache, chatConfirmations) + createMailFeature(database, userCache, chatConfirmations, wiretap) createMessageFeature(messenger, chatConfirmations, wiretap) createNicknameFeature( database, userCache, diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 7bbd7af..8708c60 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -288,7 +288,7 @@ private class BubbleCommand( @CommandAlias("shout") @Description("Send a message to global chat when in a bubble") fun shout(sender: Player, message: String) { - chatConfirmations.submit(sender, message) { + chatConfirmations.submit(sender, message) { sender -> messenger.broadcastChatMessage(sender, message) if (sender.uniqueId in messenger.excludedFromGlobalChat) { sender.sendMessage( diff --git a/chattore/src/main/kotlin/feature/Chat.kt b/chattore/src/main/kotlin/feature/Chat.kt index f83cfca..525996e 100644 --- a/chattore/src/main/kotlin/feature/Chat.kt +++ b/chattore/src/main/kotlin/feature/Chat.kt @@ -25,12 +25,12 @@ private class ChatListener( val message = event.message val bubble = bubbleManager.getBubbleByPlayer(player) if (bubble == null) { - confirmations.submit(player, message) { + confirmations.submit(player, message) { player -> messenger.broadcastChatMessage(player, message) } return } - confirmations.submit(player, message) { + confirmations.submit(player, message) { player -> if (bubbleManager.getBubbleByPlayer(player) != bubble) { player.sendError("You are no longer in the bubble you're trying to send a message to") return@submit diff --git a/chattore/src/main/kotlin/feature/ChatConfirmations.kt b/chattore/src/main/kotlin/feature/ChatConfirmations.kt index 5fdbe04..8feaff1 100644 --- a/chattore/src/main/kotlin/feature/ChatConfirmations.kt +++ b/chattore/src/main/kotlin/feature/ChatConfirmations.kt @@ -28,13 +28,21 @@ class ChatConfirmations( .onFailure { logger.error("Invalid regex $pattern: ${it.message}") } .getOrNull() } - private val flags = ConcurrentHashMap Unit>>() + private val flags = ConcurrentHashMap Unit>>() - fun submit(player: Player, message: String, proceed: () -> Unit) { + /** + * Submit [message] for flagging. [proceed] will be called with an up-to-date instance of [player] + * upon confirmation or if no flagging happens. This is for the rare case when [player] disconnects + * after submit is called and joins back to do /confirmmessage. In that case, the Player object is invalidated + * due to disconnecting, so we can supply back a fresh one from the /confirmmessage event handler in order to + * simplify calls to [submit]. The recommended way to call this is to shadow the variable for [player] in the + * [proceed] lambda's argument. See: literally any call to this function. + */ + fun submit(player: Player, message: String, proceed: (Player) -> Unit) { val matches = regexes.filter { it.containsMatchIn(message) } if (matches.isEmpty()) { flags.remove(player.uniqueId) - proceed() + proceed(player) return } fun String.highlight(r: Regex) = r.replace(this) { match -> "${match.value}" } @@ -60,7 +68,7 @@ class ChatConfirmations( player.sendRichMessage("Override recognized") flags.remove(player.uniqueId) logger.info("${player.username} (${player.uniqueId}) FLAGGED MESSAGE OVERRIDE: $message") - proceed() + proceed(player) } } } diff --git a/chattore/src/main/kotlin/feature/Funcommands.kt b/chattore/src/main/kotlin/feature/Funcommands.kt index 325f4fc..41fd8c9 100644 --- a/chattore/src/main/kotlin/feature/Funcommands.kt +++ b/chattore/src/main/kotlin/feature/Funcommands.kt @@ -126,7 +126,7 @@ private fun createFunCommands( "arg-2" toS (args.getOrNull(2) ?: ""), ) - chatConfirmations.submit(source, "/${invocation.alias()} $allArgs") { + chatConfirmations.submit(source, "/${invocation.alias()} $allArgs") { source -> cmd.globalChat?.let { proxy.all.sendRichMessage(it, *replacements) } cmd.localChat?.let { source.sendRichMessage(it, *replacements) } cmd.othersChat?.let { proxy.allBut(source).sendRichMessage(it, *replacements) } diff --git a/chattore/src/main/kotlin/feature/HelpOp.kt b/chattore/src/main/kotlin/feature/HelpOp.kt index 5ce28a3..dd367cb 100644 --- a/chattore/src/main/kotlin/feature/HelpOp.kt +++ b/chattore/src/main/kotlin/feature/HelpOp.kt @@ -25,7 +25,7 @@ private class HelpOp( @Syntax("[message]") fun default(player: Player, statement: String) { if (statement.isEmpty()) throw ChattoreException("You have to have a problem first!") // : ) - chatConfirmations.submit(player, statement) { + chatConfirmations.submit(player, statement) { player -> logger.info("[HelpOp] ${player.username}: $statement") proxy.all { it.hasChattorePrivilege || it.uniqueId == player.uniqueId } .sendRichMessage( diff --git a/chattore/src/main/kotlin/feature/Mail.kt b/chattore/src/main/kotlin/feature/Mail.kt index f9d3c40..f4eb5a7 100644 --- a/chattore/src/main/kotlin/feature/Mail.kt +++ b/chattore/src/main/kotlin/feature/Mail.kt @@ -20,8 +20,9 @@ fun PluginScope.createMailFeature( database: Storage, userCache: UserCache, chatConfirmations: ChatConfirmations, + wiretap: Wiretap, ) { - registerCommands(Mail(database, userCache, chatConfirmations)) + registerCommands(Mail(database, userCache, chatConfirmations, wiretap)) registerListeners(MailListener(plugin, database, proxy)) } @@ -99,6 +100,7 @@ private class Mail( private val database: Storage, private val userCache: UserCache, private val chatConfirmations: ChatConfirmations, + private val wiretap: Wiretap, ) : BaseCommand() { private val mailTimeouts = mutableMapOf() @@ -124,7 +126,7 @@ private class Mail( } val targetUuid = userCache.uuidOrNull(target) ?: throw ChattoreException("We do not recognize that user!") - chatConfirmations.submit(player, message) { + chatConfirmations.submit(player, message) { player -> mailTimeouts[player.uniqueId] = now database.insertMessage(player.uniqueId, targetUuid, message) player.sendRichMessage( @@ -132,6 +134,10 @@ private class Mail( "message" toS message, "recipient" toS target, ) + wiretap( + "[From to ] " + .render("message" toS message, "sender" toS player.username, "recipient" toS target), + ) } } diff --git a/chattore/src/main/kotlin/feature/Message.kt b/chattore/src/main/kotlin/feature/Message.kt index 72eb425..7f07881 100644 --- a/chattore/src/main/kotlin/feature/Message.kt +++ b/chattore/src/main/kotlin/feature/Message.kt @@ -45,7 +45,7 @@ fun PluginScope.createMessageFeature( @Syntax("[target] ") fun default(sender: Player, recipient: OnlinePlayer, message: String) { val recipientUuid = recipient.player.uniqueId - chatConfirmations.submit(sender, message) { + chatConfirmations.submit(sender, message) { sender -> val recipientPlayer = proxy.playerOrNull(recipientUuid) ?: throw ChattoreException("The person you're trying to message is no longer online!") sendMessage(sender, recipientPlayer, message) @@ -59,7 +59,7 @@ fun PluginScope.createMessageFeature( @Default fun default(sender: Player, message: String) { val recipientUuid = replyMap[sender.uniqueId] ?: throw ChattoreException("You have no one to reply to!") - chatConfirmations.submit(sender, message) { + chatConfirmations.submit(sender, message) { sender -> val recipient = proxy.playerOrNull(recipientUuid) ?: throw ChattoreException("The person you are trying to reply to is no longer online!") sendMessage(sender, recipient, message) diff --git a/chattore/src/main/kotlin/feature/Profile.kt b/chattore/src/main/kotlin/feature/Profile.kt index 980300a..3be9ede 100644 --- a/chattore/src/main/kotlin/feature/Profile.kt +++ b/chattore/src/main/kotlin/feature/Profile.kt @@ -43,7 +43,7 @@ private class Profile( @Subcommand("about") @CommandPermission("chattore.profile.about") fun about(player: Player, about: String) { - chatConfirmations.submit(player, "/$execCommandLabel about $about") { + chatConfirmations.submit(player, "/$execCommandLabel about $about") { player -> database.setAbout(player.uniqueId, about) player.sendInfo("Set your about to '$about'.") } From 80170781560cadbca80a555f8f039d207aa71117 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:10:50 +0300 Subject: [PATCH 62/63] Update Gradle and Kotlin --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 515d894..76425a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.1.21" +kotlin = "2.4.0" kotlinx-serialization = "1.8.1" exposed = "0.58.0" jackson = "2.19.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a006b7f..76dffd0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jan 21 15:46:13 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 88b6852db9dbcb201190f201ab3c43def213f212 Mon Sep 17 00:00:00 2001 From: Pauli Kauro <3965357+paulikauro@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:11:08 +0300 Subject: [PATCH 63/63] Use explicit backing field in BubbleManager Stable in Kotlin 2.4.0. IDEA's editor complains about it until next release (IDEA 2026.1.4). It's a false positive, the feature is no longer experimental. --- chattore/src/main/kotlin/feature/Bubble.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chattore/src/main/kotlin/feature/Bubble.kt b/chattore/src/main/kotlin/feature/Bubble.kt index 8708c60..c36f83e 100644 --- a/chattore/src/main/kotlin/feature/Bubble.kt +++ b/chattore/src/main/kotlin/feature/Bubble.kt @@ -343,15 +343,15 @@ class Bubble( } class BubbleManager { - private val _bubbles: MutableList = mutableListOf() - val bubbles: List get() = _bubbles + val bubbles: List + field = mutableListOf() fun createBubble(player: UUID): Bubble = - Bubble(player, mutableSetOf(player), mutableSetOf(), isPrivate = true).also(_bubbles::add) + Bubble(player, mutableSetOf(player), mutableSetOf(), isPrivate = true).also(bubbles::add) fun removeBubble(bubble: Bubble) { - _bubbles.remove(bubble) + bubbles.remove(bubble) } - fun getBubbleByPlayer(player: Player): Bubble? = _bubbles.firstOrNull { player.uniqueId in it.players } + fun getBubbleByPlayer(player: Player): Bubble? = bubbles.firstOrNull { player.uniqueId in it.players } }