diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-26 02:34:01 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-26 02:34:58 +0900 |
| commit | a7cac6e6b49c017b20c9c1f63b09ae06dc5605a6 (patch) | |
| tree | ae263379b1d89886d699841869a9effa35bfc87b /platform-paper/src | |
| parent | 64abf8c7a06839fdf662cb0e781ed1dc14da16a9 (diff) | |
| download | LunaticChat-a7cac6e6b49c017b20c9c1f63b09ae06dc5605a6.tar.gz LunaticChat-a7cac6e6b49c017b20c9c1f63b09ae06dc5605a6.tar.bz2 LunaticChat-a7cac6e6b49c017b20c9c1f63b09ae06dc5605a6.zip | |
refactor: Refactoring logic
Diffstat (limited to 'platform-paper/src')
16 files changed, 199 insertions, 78 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt index 39afbc1..35f70d8 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt @@ -257,6 +257,6 @@ class ServiceInitializer( services.playerSettingsManager.saveToDisk() conversionCache?.saveToDisk() services.channelManager?.saveToDisk() - services.chatModeManager?.saveToDisk() + services.chatModeManager?.shutdown() } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt index 5f92f21..93a7f04 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt @@ -91,4 +91,13 @@ class ChatModeManager( val data = ChatModeData(modes = chatModes.toMap()) storage.saveToDisk(data) } + + /** + * Shuts down the chat mode manager and its storage executor. + * Should be called during plugin disable to prevent thread leaks. + */ + fun shutdown() { + saveToDisk() + storage.shutdown() + } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt index f7d0812..fc8d6da 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt @@ -9,6 +9,7 @@ import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import java.util.UUID import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import java.util.logging.Logger import kotlin.collections.forEach @@ -17,7 +18,7 @@ class ChannelManager( private val logger: Logger, ) { private val channelsCache = ConcurrentHashMap<String, Channel>() - private val membersCache = ConcurrentHashMap<String, MutableList<ChannelMember>>() + private val membersCache = ConcurrentHashMap<String, CopyOnWriteArrayList<ChannelMember>>() private val activeChannels = ConcurrentHashMap<UUID, String>() /** @@ -27,7 +28,7 @@ class ChannelManager( val data = storage.loadFromDisk() channelsCache.putAll(data.channels) data.members.forEach { (channelId, members) -> - membersCache[channelId] = members.toMutableList() + membersCache[channelId] = CopyOnWriteArrayList(members) } data.activeChannels.forEach { (playerIdStr, channelId) -> try { @@ -60,7 +61,7 @@ class ChannelManager( playerId = channel.ownerId, role = ChannelRole.OWNER, ) - membersCache[channel.id] = mutableListOf(ownerMember) + membersCache[channel.id] = CopyOnWriteArrayList(listOf(ownerMember)) // Set the owner's active channel setPlayerChannel(channel.ownerId, channel.id) @@ -169,7 +170,7 @@ class ChannelManager( val members = membersCache.getOrPut(channelId) { - mutableListOf() + CopyOnWriteArrayList() } val newMember = ChannelMember( diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt index cf57991..0560939 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt @@ -16,7 +16,7 @@ class ChannelMessageHandler( ) { private var lunaticChatConfiguration = ConfigManager.getConfiguration() - suspend fun sendChannelMessage( + fun sendChannelMessage( player: Player, message: String, ): Boolean { @@ -33,7 +33,7 @@ class ChannelMessageHandler( .forEach { it.sendMessage(formattedMessage) } context.members.forEach { member -> Bukkit.getPlayer(member.playerId)?.let { memberPlayer -> - if (memberPlayer.isOnline && member.playerId != playerId) { + if (memberPlayer.isOnline) { memberPlayer.sendMessage(formattedMessage) } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt index 7d06ffd..7857221 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt @@ -59,10 +59,18 @@ class DirectMessageHandler( /** * Clears message history for a player (called on disconnect). + * Removes entries where this player is either the sender or recipient. */ fun clearPlayer(player: Player) { - lastMessager.remove(player.uniqueId) - lastRecipient.remove(player.uniqueId) + val playerId = player.uniqueId + + // Remove entries where this player is the sender + lastMessager.remove(playerId) + lastRecipient.remove(playerId) + + // Remove entries where this player is the recipient + lastMessager.entries.removeIf { it.value == playerId } + lastRecipient.entries.removeIf { it.value == playerId } } /** @@ -72,7 +80,7 @@ class DirectMessageHandler( * * @return true if message was sent successfully */ - suspend fun sendDirectMessage( + fun sendDirectMessage( sender: Player, recipient: Player, message: String, @@ -82,15 +90,20 @@ class DirectMessageHandler( val senderSettings = settingsManager?.getSettings(sender.uniqueId) val recipientSettings = settingsManager?.getSettings(recipient.uniqueId) + // Handle romaji conversion if enabled (requires blocking for HTTP call) val displayMessage = - senderSettings - ?.takeIf { it.japaneseConversionEnabled } - ?.let { romanjiConverter } - ?.runCatching { - convert(message) - ?.let { "$message §e($it)" } - ?: message - }?.getOrNull() ?: message + if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) { + runCatching { + kotlinx.coroutines.runBlocking { + romanjiConverter + ?.convert(message) + ?.let { "$message §e($it)" } + ?: message + } + }.getOrNull() ?: message + } else { + message + } val format = lunaticChatConfiguration.messageFormat.directMessageFormat diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt index acf8fcf..e1a997e 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt @@ -15,7 +15,6 @@ import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands -import kotlinx.coroutines.runBlocking @Command( name = "reply", @@ -62,9 +61,7 @@ class ReplyCommand( ), ) - runBlocking { - dmHandler.sendDirectMessage(sender, target, message) - } + dmHandler.sendDirectMessage(sender, target, message) return CommandResult.Success } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt index 16311cf..5375b08 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt @@ -17,7 +17,6 @@ import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands -import kotlinx.coroutines.runBlocking import org.bukkit.Bukkit import java.util.concurrent.CompletableFuture @@ -81,9 +80,7 @@ class TellCommand( ) } - runBlocking { - directMessageHandler.sendDirectMessage(sender, recipient, message) - } + directMessageHandler.sendDirectMessage(sender, recipient, message) return CommandResult.Success } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt index 3617df1..9aa563a 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt @@ -8,6 +8,8 @@ import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig import dev.m1sk9.lunaticChat.paper.i18n.Language import org.bukkit.configuration.file.FileConfiguration +// FIXME: ConfigManager uses mutable static state which makes testing difficult +// and creates hidden global dependencies. Consider refactoring to dependency injection. object ConfigManager { private var lunaticChatConfiguration: LunaticChatConfiguration? = null diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt index d2ba632..da79cfa 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt @@ -110,17 +110,20 @@ class ConversionCache( private fun queueSaveToDisk() { if (conversionSaveQueue.compareAndSet(false, true)) { - Bukkit.getScheduler().runTaskAsynchronously( + Bukkit.getScheduler().runTaskLaterAsynchronously( plugin, Runnable { - Thread.sleep(5000) // 5 seconds delay to batch multiple save requests - conversionSaveQueue.set(true) + conversionSaveQueue.set(false) saveToDisk() }, + 100L, // 5 seconds = 100 ticks ) } } + // FIXME: ConcurrentHashMap keys are unordered, so evicting "oldest" entries + // actually evicts random entries. Consider using LinkedHashMap with access-order + // or implement proper LRU cache with timestamp tracking. private fun evictOldestEntry() { val toRemove = conversionMemoryCache.size / 10 conversionMemoryCache.keys.take(toRemove).forEach { diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt index 2c22be1..f8e8181 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt @@ -29,7 +29,14 @@ object EventListenerRegistry { // Always register these listeners pluginManager.registerEvents(SpyPermissionManager, plugin) pluginManager.registerEvents( - PlayerPresenceListener(plugin, services.languageManager, updateAvailable), + PlayerPresenceListener( + lunaticChat = plugin, + languageManager = services.languageManager, + updateCheckerFlag = updateAvailable, + playerSettingsManager = services.playerSettingsManager, + chatModeManager = services.chatModeManager, + channelManager = services.channelManager, + ), plugin, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt index 258b0b1..47c8cd4 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt @@ -21,47 +21,58 @@ class PlayerChatListener( private val plainTextSerializer = PlainTextComponentSerializer.plainText() @EventHandler(ignoreCancelled = true) - fun onChat(event: AsyncChatEvent) = - runBlocking { - val player = event.player - val settings = settingsManager.getSettings(player.uniqueId) + fun onChat(event: AsyncChatEvent) { + val player = event.player + val settings = settingsManager.getSettings(player.uniqueId) - val originalMessage = plainTextSerializer.serialize(event.message()) - val displayMessage = - settings - .takeIf { - it.japaneseConversionEnabled - }?.runCatching { - romajiConverter - .convert(originalMessage) - ?.let { "$originalMessage §e($it)" } - ?: originalMessage - }?.getOrNull() ?: originalMessage + val originalMessage = plainTextSerializer.serialize(event.message()) - val (_, effectiveMode) = - if (originalMessage.startsWith('!')) { - val withoutPrefix = originalMessage.removePrefix("!").trim() - if (withoutPrefix.isEmpty()) { - event.isCancelled = true - return@runBlocking - } - val currentMode = chatModeManager.getChatMode(player.uniqueId) - withoutPrefix to currentMode.toggle() - } else { - originalMessage to chatModeManager.getChatMode(player.uniqueId) - } + // Handle chat mode switching with '!' prefix + val hasPrefix = originalMessage.startsWith('!') + val messageWithoutPrefix = + if (hasPrefix) { + originalMessage.removePrefix("!").trim() + } else { + originalMessage + } - when (effectiveMode) { - ChatMode.GLOBAL -> { - event.message(Component.text(displayMessage)) - } - ChatMode.CHANNEL -> { - event.isCancelled = true - channelMessageHandler - .sendChannelMessage(player, originalMessage) - } + if (hasPrefix && messageWithoutPrefix.isEmpty()) { + event.isCancelled = true + return + } + + val effectiveMode = + if (hasPrefix) { + val currentMode = chatModeManager.getChatMode(player.uniqueId) + currentMode.toggle() + } else { + chatModeManager.getChatMode(player.uniqueId) } - event.message(Component.text(displayMessage)) + // Handle romaji conversion if enabled (requires blocking for HTTP call) + val displayMessage = + if (settings.japaneseConversionEnabled) { + runCatching { + runBlocking { + romajiConverter + .convert(messageWithoutPrefix) + ?.let { "$messageWithoutPrefix §e($it)" } + ?: messageWithoutPrefix + } + }.getOrNull() ?: messageWithoutPrefix + } else { + messageWithoutPrefix + } + + // Route message based on chat mode + when (effectiveMode) { + ChatMode.GLOBAL -> { + event.message(Component.text(displayMessage)) + } + ChatMode.CHANNEL -> { + event.isCancelled = true + channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix) + } } + } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt index ca73567..fe4239d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt @@ -1,9 +1,14 @@ package dev.m1sk9.lunaticChat.paper.listener +import dev.m1sk9.lunaticChat.engine.chat.ChatMode import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.common.hasAnyPermission import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import org.bukkit.event.EventHandler @@ -16,24 +21,72 @@ class PlayerPresenceListener( private val lunaticChat: LunaticChat, private val languageManager: LanguageManager, private val updateCheckerFlag: AtomicBoolean, + private val playerSettingsManager: PlayerSettingsManager, + private val chatModeManager: ChatModeManager? = null, + private val channelManager: ChannelManager? = null, ) : Listener { @EventHandler(ignoreCancelled = true) fun onJoin(event: PlayerJoinEvent) { val player = event.player - if (!updateCheckerFlag.get() || !player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) return - - player.sendMessage { - Component - .text( - languageManager.getMessage("general.newUpdateAvailable"), - ).clickEvent( - ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest"), + + // Send update notification if available + if (updateCheckerFlag.get() && player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) { + player.sendMessage { + Component + .text( + languageManager.getMessage("general.newUpdateAvailable"), + ).clickEvent( + ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest"), + ) + } + } + + // Send chat mode notification + chatModeManager?.let { manager -> + val chatMode = manager.getChatMode(player.uniqueId) + val modeKey = + when (chatMode) { + ChatMode.GLOBAL -> "chatmode.mode.global" + ChatMode.CHANNEL -> "chatmode.mode.channel" + } + val modeText = languageManager.getMessage(modeKey) + val notification = + languageManager.getMessage( + "chatmode.notification.login", + mapOf("mode" to modeText), ) + player.sendMessage(MessageFormatter.format(notification)) + } + + // Send channel notification if in a channel + channelManager?.let { manager -> + val context = manager.getPlayerChannelContext(player.uniqueId) + context?.let { + val notification = + languageManager.getMessage( + "channel.notification.login", + mapOf("channelName" to it.channel.name), + ) + player.sendMessage(Component.text(notification)) + } } } @EventHandler(ignoreCancelled = true) fun onQuit(event: PlayerQuitEvent) { - lunaticChat.directMessageHandler.clearPlayer(event.player) + val player = event.player + val playerId = player.uniqueId + + // 1. Clear direct message references + lunaticChat.directMessageHandler.clearPlayer(player) + + // 2. Clear active channel for this player + channelManager?.setPlayerChannel(playerId, null) + + // 3. Trigger async save of chat mode data + chatModeManager?.saveToDisk() + + // 4. Trigger async save of player settings + playerSettingsManager.saveToDisk() } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt index 754c82b..0aa8eef 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt @@ -87,13 +87,13 @@ class YamlPlayerSettingsStorage( */ fun queueAsyncSave(data: PlayerSettingsData) { if (saveFlag.compareAndSet(false, true)) { - Bukkit.getScheduler().runTaskAsynchronously( + Bukkit.getScheduler().runTaskLaterAsynchronously( plugin, Runnable { - Thread.sleep(5000) // 5 seconds delay to batch multiple save requests saveFlag.set(false) saveToDisk(data) }, + 100L, // 5 seconds = 100 ticks ) } } diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml index 91a4f3b..39908d2 100644 --- a/platform-paper/src/main/resources/languages/en.yml +++ b/platform-paper/src/main/resources/languages/en.yml @@ -85,6 +85,8 @@ channel: noChannels: "You are not a member of any channels" clickToSwitch: "Click to switch" error: "Failed to retrieve channel status" + notification: + login: "You are in channel '{channelName}'" chatmode: current: "Current chat mode" @@ -93,6 +95,8 @@ chatmode: channel: "CHANNEL" toggle: success: "Chat mode switched to" + notification: + login: "Current chat mode: {mode}" general: playerOnlyCommand: "This command can only be executed by players." diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml index 9e93ce9..47399b8 100644 --- a/platform-paper/src/main/resources/languages/ja.yml +++ b/platform-paper/src/main/resources/languages/ja.yml @@ -85,6 +85,8 @@ channel: noChannels: "どのチャンネルにも参加していません" clickToSwitch: "クリックして切り替え" error: "チャンネルステータスの取得に失敗しました" + notification: + login: "チャンネル '{channelName}' に入室中です" chatmode: current: "現在のチャットモード" @@ -93,6 +95,8 @@ chatmode: channel: "チャンネル" toggle: success: "チャットモードを切り替えました" + notification: + login: "現在のチャットモード: {mode}" general: playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます" diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml index 3f991a9..b3a9d8f 100644 --- a/platform-paper/src/main/resources/paper-plugin.yml +++ b/platform-paper/src/main/resources/paper-plugin.yml @@ -19,6 +19,26 @@ permissions: default: true lunaticchat.command.lc.status: default: true + lunaticchat.command.lc.channel: + default: true + lunaticchat.command.lc.channel.create: + default: true + lunaticchat.command.lc.channel.list: + default: true + lunaticchat.command.lc.channel.join: + default: true + lunaticchat.command.lc.channel.leave: + default: true + lunaticchat.command.lc.channel.switch: + default: true + lunaticchat.command.lc.channel.status: + default: true + lunaticchat.command.lc.channel.delete: + default: true + lunaticchat.command.lc.chatmode: + default: true + lunaticchat.command.lc.chatmode.toggle: + default: true lunaticchat.spy: default: op |
