diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 22:41:42 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-02 22:41:42 +0900 |
| commit | f7573a136c9fb6803dd84b78c2b39e9266e76f32 (patch) | |
| tree | adcabcffed7778719f60c9a9cdf68d6758816579 | |
| parent | 1dd2787059331db57049cb1dac8a38ceea859992 (diff) | |
| parent | f813fc8c0e2ff48c1dc087733d8d250797a2cd3c (diff) | |
| download | LunaticChat-f7573a136c9fb6803dd84b78c2b39e9266e76f32.tar.gz LunaticChat-f7573a136c9fb6803dd84b78c2b39e9266e76f32.tar.bz2 LunaticChat-f7573a136c9fb6803dd84b78c2b39e9266e76f32.zip | |
Merge pull request #260 from m1sk9/refactor/codebase-cleanup
refactor: single-source duplicated command, setting, and cross-server logic
82 files changed, 1476 insertions, 2718 deletions
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt index 02dc1d3..54b98e0 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt @@ -1,7 +1,12 @@ package dev.m1sk9.lunaticChat.engine.chat.channel +/** + * A player's active channel together with its member list. + */ data class ChannelContext( - val channelId: String, val channel: Channel, val members: List<ChannelMember>, -) +) { + /** Shorthand for the channel's id, which callers ask for far more often than the channel. */ + val channelId: String get() = channel.id +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt index 24e3560..bdbdf69 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt @@ -4,16 +4,10 @@ package dev.m1sk9.lunaticChat.engine.converter * Converts romanji text to hiragana using Trie data structure. */ object KanaConverter { - sealed class TrieNode { - data class Leaf( - val value: String, - ) : TrieNode() - - data class Branch( - val children: Map<Char, TrieNode>, - val value: String? = null, - ) : TrieNode() - } + private class TrieNode( + val children: Map<Char, TrieNode>, + val value: String? = null, + ) private val romanjiTrie: TrieNode = buildTrie() @@ -216,7 +210,7 @@ object KanaConverter { "n" to "ん", ) - return insertAll(TrieNode.Branch(emptyMap()), mappings) + return insertAll(TrieNode(emptyMap()), mappings) } private fun insertAll( @@ -235,22 +229,33 @@ object KanaConverter { key: String, value: String, ): TrieNode { - if (key.isEmpty()) { - return when (node) { - is TrieNode.Branch -> TrieNode.Branch(node.children, value) - is TrieNode.Leaf -> TrieNode.Leaf(value) - } - } + if (key.isEmpty()) return TrieNode(node.children, value) - return when (node) { - is TrieNode.Branch -> { - val char = key[0] - val child = node.children[char] ?: TrieNode.Branch(emptyMap()) - val newChild = insert(child, key.substring(1), value) - TrieNode.Branch(node.children + (char to newChild), node.value) - } - is TrieNode.Leaf -> node + val char = key[0] + val child = node.children[char] ?: TrieNode(emptyMap()) + return TrieNode(node.children + (char to insert(child, key.substring(1), value)), node.value) + } + + /** + * Walks the trie from [start] and returns the longest mapping that matches, paired with the + * number of characters it consumed, or null when no prefix of the input maps to kana. + */ + private fun longestMatch( + input: String, + start: Int, + ): Pair<String, Int>? { + var node = romanjiTrie + var match: Pair<String, Int>? = null + var i = start + + while (true) { + node.value?.let { match = it to (i - start) } + if (i >= input.length) break + node = node.children[input[i]] ?: break + i++ } + + return match } /** @@ -282,39 +287,8 @@ object KanaConverter { } } - // Try to find the longest match in the trie - var node: TrieNode = romanjiTrie - var matchLength = 0 - var j = i - - while (j < lowerInput.length && lowerInput[j] in 'a'..'z') { - node = - when (node) { - is TrieNode.Branch -> { - if (node.value != null) { - matchLength = j - i - } - node.children[lowerInput[j]] ?: break - } - is TrieNode.Leaf -> { - matchLength = j - i - break - } - } - j++ - } - - // Check for terminal match - if (node is TrieNode.Leaf) { - matchLength = j - i - } else if (node is TrieNode.Branch && node.value != null) { - matchLength = j - i - } - // If no match found, this character cannot be converted - not valid romaji - if (matchLength == 0) { - return false - } + val (_, matchLength) = longestMatch(lowerInput, i) ?: return false i += matchLength } @@ -344,37 +318,10 @@ object KanaConverter { } } - var node: TrieNode = romanjiTrie - var lastMatch: Pair<String, Int>? = null - var j = i - - while (j < lowerInput.length) { - node = - when (node) { - is TrieNode.Branch -> { - if (node.value != null) { - lastMatch = node.value to (j - i) - } - - node.children[lowerInput[j]] ?: break - } - is TrieNode.Leaf -> { - lastMatch = node.value to (j - i) - break - } - } - j++ - } - - if (node is TrieNode.Leaf) { - lastMatch = node.value to (j - i) - } else if (node is TrieNode.Branch && node.value != null) { - lastMatch = node.value to (j - i) - } - - if (lastMatch != null) { - result.append(lastMatch.first) - i += lastMatch.second + val match = longestMatch(lowerInput, i) + if (match != null) { + result.append(match.first) + i += match.second } else { result.append(lowerInput[i]) i++ diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageChannel.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageChannel.kt new file mode 100644 index 0000000..85093ed --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageChannel.kt @@ -0,0 +1,16 @@ +package dev.m1sk9.lunaticChat.engine.protocol + +/** + * The plugin messaging channel Paper and Velocity exchange [PluginMessage]s over. + * + * Both sides must agree on this exactly. Declaring it next to the codec keeps a rename from + * silently splitting the two halves of the protocol: a Paper server and a proxy that disagree + * still compile and start, they just stop talking. + */ +object PluginMessageChannel { + const val NAMESPACE = "lunaticchat" + const val NAME = "main" + + /** The channel in Bukkit's `namespace:name` form. */ + const val ID = "$NAMESPACE:$NAME" +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt index 0e3cd24..91c37b8 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt @@ -30,17 +30,17 @@ import java.util.UUID data class PlayerSettingsData( val version: Int = 1, val japaneseConversion: Map< - @Serializable(with = UUIDASStringSerializer::class) + @Serializable(with = UUIDSerializer::class) UUID, Boolean, > = emptyMap(), val directMessageNotification: Map< - @Serializable(with = UUIDASStringSerializer::class) + @Serializable(with = UUIDSerializer::class) UUID, Boolean, > = emptyMap(), val channelMessageNotification: Map< - @Serializable(with = UUIDASStringSerializer::class) + @Serializable(with = UUIDSerializer::class) UUID, Boolean, > = emptyMap(), diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt deleted file mode 100644 index 7e6b331..0000000 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.m1sk9.lunaticChat.engine.settings - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import java.util.UUID - -/** - * Serializer for UUID that converts to/from String format for YAML compatibility. - * Used in PlayerSettingsData for serializing UUID keys in maps. - */ -object UUIDASStringSerializer : KSerializer<UUID> { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("UUIDAsString", PrimitiveKind.STRING) - - override fun serialize( - encoder: Encoder, - value: UUID, - ) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString()) -} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt index e03c1c1..dd01616 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt @@ -11,6 +11,8 @@ import java.util.UUID /** * Custom serializer for UUID with kotlinx.serialization. * kotlinx.serialization doesn't support UUID by default, so we need a custom serializer. + * + * Used for both UUID properties and UUID map keys (JSON and YAML alike). */ object UUIDSerializer : KSerializer<UUID> { override val descriptor: SerialDescriptor = diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt index 4684432..de514b1 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelDataClassesTest.kt @@ -114,7 +114,7 @@ class ChannelDataClassesTest { fun `ChannelContext should store all fields`() { val channel = Channel(id = "ch-1", name = "Test", ownerId = testOwnerId) val member = ChannelMember(channelId = "ch-1", playerId = testPlayerId, role = ChannelRole.MEMBER) - val context = ChannelContext(channelId = "ch-1", channel = channel, members = listOf(member)) + val context = ChannelContext(channel = channel, members = listOf(member)) assertEquals("ch-1", context.channelId) assertEquals(channel, context.channel) @@ -125,8 +125,8 @@ class ChannelDataClassesTest { @Test fun `ChannelContext copy should create independent instance`() { val channel = Channel(id = "ch-1", name = "Test", ownerId = testOwnerId) - val original = ChannelContext(channelId = "ch-1", channel = channel, members = emptyList()) - val copied = original.copy(channelId = "ch-2") + val original = ChannelContext(channel = channel, members = emptyList()) + val copied = original.copy(channel = channel.copy(id = "ch-2")) assertEquals("ch-2", copied.channelId) assertNotEquals(original.channelId, copied.channelId) diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt index 599a372..58d9c32 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt @@ -16,12 +16,6 @@ class UUIDSerializerTest { val uuid: UUID, ) - @Serializable - private data class UUIDAsStringHolder( - @Serializable(with = UUIDASStringSerializer::class) - val uuid: UUID, - ) - @Test fun `UUIDSerializer should serialize UUID to string`() { val uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc") @@ -52,35 +46,6 @@ class UUIDSerializerTest { } @Test - fun `UUIDASStringSerializer should serialize UUID to string`() { - val uuid = UUID.fromString("abcdef01-2345-6789-abcd-ef0123456789") - val holder = UUIDAsStringHolder(uuid) - - val jsonString = json.encodeToString(UUIDAsStringHolder.serializer(), holder) - - assert(jsonString.contains("abcdef01-2345-6789-abcd-ef0123456789")) - } - - @Test - fun `UUIDASStringSerializer should deserialize string to UUID`() { - val jsonString = """{"uuid":"abcdef01-2345-6789-abcd-ef0123456789"}""" - val holder = json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) - - assertEquals(UUID.fromString("abcdef01-2345-6789-abcd-ef0123456789"), holder.uuid) - } - - @Test - fun `UUIDASStringSerializer round-trip should preserve UUID`() { - val originalUuid = UUID.randomUUID() - val holder = UUIDAsStringHolder(originalUuid) - - val jsonString = json.encodeToString(UUIDAsStringHolder.serializer(), holder) - val decoded = json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) - - assertEquals(originalUuid, decoded.uuid) - } - - @Test fun `UUIDSerializer should fail on invalid UUID string`() { val jsonString = """{"uuid":"not-a-valid-uuid"}""" @@ -88,13 +53,4 @@ class UUIDSerializerTest { json.decodeFromString(UUIDHolder.serializer(), jsonString) } } - - @Test - fun `UUIDASStringSerializer should fail on invalid UUID string`() { - val jsonString = """{"uuid":"not-a-valid-uuid"}""" - - assertFailsWith<Exception> { - json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) - } - } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt new file mode 100644 index 0000000..bc31785 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt @@ -0,0 +1,36 @@ +package dev.m1sk9.lunaticChat.paper + +import org.bukkit.plugin.java.JavaPlugin +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Coalesces a burst of save requests into one asynchronous write. + * + * The first [request] after an idle period schedules the write [delaySeconds] later; requests + * arriving before it fires are absorbed by it, so a player toggling a setting repeatedly costs one + * file write rather than one per toggle. + */ +class DebouncedSaver( + private val plugin: JavaPlugin, + private val delaySeconds: Long = 5, +) { + private val pending = AtomicBoolean(false) + + /** + * Schedules [save] to run asynchronously, unless a write is already pending. + */ + fun request(save: () -> Unit) { + if (!pending.compareAndSet(false, true)) return + + plugin.server.asyncScheduler.runDelayed( + plugin, + { + pending.set(false) + save() + }, + delaySeconds, + TimeUnit.SECONDS, + ) + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt index 035c578..5b8cab1 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt @@ -2,7 +2,6 @@ package dev.m1sk9.lunaticChat.paper import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry @@ -10,10 +9,9 @@ import dev.m1sk9.lunaticChat.paper.command.impl.ReplyCommand import dev.m1sk9.lunaticChat.paper.command.impl.TellCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.LunaticChatCommand import dev.m1sk9.lunaticChat.paper.command.impl.lcv.VelocityStatusCommand +import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandler import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandlerRegistry -import dev.m1sk9.lunaticChat.paper.command.setting.handler.ChannelMessageNoticeSettingHandler -import dev.m1sk9.lunaticChat.paper.command.setting.handler.DirectMessageNoticeSettingHandler -import dev.m1sk9.lunaticChat.paper.command.setting.handler.JapaneseConversionSettingHandler +import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey import dev.m1sk9.lunaticChat.paper.common.UpdateCheckResult import dev.m1sk9.lunaticChat.paper.common.UpdateChecker import dev.m1sk9.lunaticChat.paper.config.ConfigManager @@ -31,13 +29,13 @@ import java.util.concurrent.atomic.AtomicBoolean class LunaticChat : JavaPlugin(), Listener { - lateinit var directMessageHandler: DirectMessageHandler - lateinit var languageManager: LanguageManager - var channelManager: ChannelManager? = null - var channelMembershipManager: ChannelMembershipManager? = null - var channelMessageHandler: ChannelMessageHandler? = null - var channelNotificationHandler: ChannelNotificationHandler? = null - var velocityConnectionManager: VelocityConnectionManager? = null + // Read by commands, which reach the plugin instance but not the container. + val directMessageHandler: DirectMessageHandler get() = services.directMessageHandler + val languageManager: LanguageManager get() = services.languageManager + val channelManager: ChannelManager? get() = services.channelManager + val channelMembershipManager: ChannelMembershipManager? get() = services.channelMembershipManager + val channelNotificationHandler: ChannelNotificationHandler? get() = services.channelNotificationHandler + val velocityConnectionManager: VelocityConnectionManager? get() = services.velocityConnectionManager private lateinit var services: ServiceContainer private lateinit var configuration: LunaticChatConfiguration @@ -60,7 +58,7 @@ class LunaticChat : val httpClient = HttpClient(CIO) // Initialize plugin coroutine scope - pluginScope = PluginCoroutineScope(this, logger) + pluginScope = PluginCoroutineScope(logger) // Initialize all services serviceInitializer = @@ -72,17 +70,8 @@ class LunaticChat : ) services = serviceInitializer.initialize() - // Set public API properties (for command access) - directMessageHandler = services.directMessageHandler - languageManager = services.languageManager - channelManager = services.channelManager - channelMembershipManager = services.channelMembershipManager - channelMessageHandler = services.channelMessageHandler - channelNotificationHandler = services.channelNotificationHandler - velocityConnectionManager = services.velocityConnectionManager - // Schedule periodic tasks - serviceInitializer.schedulePeriodicTasks() + serviceInitializer.schedulePeriodicTasks(services) // Register commands and listeners registerCommands() @@ -109,31 +98,16 @@ class LunaticChat : val commandRegistry = CommandRegistry(this) val settingHandlerRegistry = SettingHandlerRegistry() - // Always register DM notification setting - settingHandlerRegistry.register( - DirectMessageNoticeSettingHandler( - services.playerSettingsManager, - services.languageManager, - ), - ) - - // Always register channel message notification setting if channel is enabled - if (services.channelManager != null) { - settingHandlerRegistry.register( - ChannelMessageNoticeSettingHandler( - services.playerSettingsManager, - services.languageManager, - ), - ) - } - - // Conditionally register Japanese conversion setting - if (services.romajiConverter != null) { + // DM notification is always available; the other two follow their feature + val enabledSettings = + buildList { + add(SettingKey.Notice) + if (services.channelManager != null) add(SettingKey.ChNotice) + if (services.romajiConverter != null) add(SettingKey.Japanese) + } + enabledSettings.forEach { key -> settingHandlerRegistry.register( - JapaneseConversionSettingHandler( - services.playerSettingsManager, - services.languageManager, - ), + SettingHandler(key, services.playerSettingsManager, services.languageManager), ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt index 114f82d..f67d262 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import org.bukkit.plugin.java.JavaPlugin import java.util.logging.Logger /** @@ -26,7 +25,6 @@ import java.util.logging.Logger * ``` */ class PluginCoroutineScope( - private val plugin: JavaPlugin, private val logger: Logger, ) { private val job = SupervisorJob() diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt index 4d668f2..48f6e37 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt @@ -2,9 +2,11 @@ package dev.m1sk9.lunaticChat.paper import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMessageLogger import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler +import dev.m1sk9.lunaticChat.paper.converter.ConversionCache import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager @@ -23,8 +25,10 @@ import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager * @property playerSettingsManager Always available (required for DM notifications) * @property directMessageHandler Always available (core feature) * @property romajiConverter Optional (only when Japanese conversion feature is enabled) + * @property conversionCache Optional (only when Japanese conversion feature is enabled) * @property channelManager Optional (only when channel chat feature is enabled) * @property channelMembershipManager Optional (only when channel chat feature is enabled) + * @property channelMessageLogger Optional (only when channel message logging is enabled) * @property channelMessageHandler Optional (only when channel chat feature is enabled) * @property channelNotificationHandler Optional (only when channel chat feature is enabled) * @property velocityConnectionManager Optional (only when Velocity integration is enabled) @@ -37,8 +41,10 @@ data class ServiceContainer( val playerSettingsManager: PlayerSettingsManager, val directMessageHandler: DirectMessageHandler, val romajiConverter: RomanjiConverter? = null, + val conversionCache: ConversionCache? = null, val channelManager: ChannelManager? = null, val channelMembershipManager: ChannelMembershipManager? = null, + val channelMessageLogger: ChannelMessageLogger? = null, val channelMessageHandler: ChannelMessageHandler? = null, val channelNotificationHandler: ChannelNotificationHandler? = null, val velocityConnectionManager: VelocityConnectionManager? = null, 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 5ed8ed6..c118c12 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 @@ -36,6 +36,7 @@ private data class ChannelComponents( val channelMembershipManager: ChannelMembershipManager, val channelMessageHandler: ChannelMessageHandler, val channelNotificationHandler: ChannelNotificationHandler, + val channelMessageLogger: ChannelMessageLogger?, ) /** @@ -50,16 +51,6 @@ class ServiceInitializer( private val httpClient: HttpClient, private val logger: Logger, ) { - private var conversionCache: ConversionCache? = null - private var channelManager: ChannelManager? = null - private var channelMembershipManager: ChannelMembershipManager? = null - private var channelMessageHandler: ChannelMessageHandler? = null - private var channelNotificationHandler: ChannelNotificationHandler? = null - private var channelMessageLogger: ChannelMessageLogger? = null - private var velocityConnectionManager: VelocityConnectionManager? = null - private var crossServerChatManager: CrossServerChatManager? = null - private var crossServerDirectMessageManager: CrossServerDirectMessageManager? = null - private var remotePlayerRegistry: RemotePlayerRegistry? = null private val handshakeCompleted = AtomicBoolean(false) /** @@ -89,12 +80,13 @@ class ServiceInitializer( val playerSettingsManager = initializePlayerSettingsManager() // 3. Initialize Japanese conversion (optional) - val romajiConverter = + val japaneseConversion = if (configuration.features.japaneseConversion.enabled) { initializeJapaneseConversion() } else { null } + val romajiConverter = japaneseConversion?.first // 4. Initialize channel manager, membership manager, channel message handler, and notification handler val channelComponents = @@ -103,11 +95,6 @@ class ServiceInitializer( } else { null } - val channelManager = channelComponents?.channelManager - val channelMembershipManager = channelComponents?.channelMembershipManager - val channelMessageHandler = channelComponents?.channelMessageHandler - val channelNotificationHandler = channelComponents?.channelNotificationHandler - // 5. Initialize handlers val directMessageHandler = DirectMessageHandler( @@ -137,26 +124,31 @@ class ServiceInitializer( } // 8. Initialize cross-server direct message manager and presence registry (optional) - if (configuration.features.velocityIntegration.enabled && - configuration.features.velocityIntegration.crossServerDirectMessage && - velocityManager != null - ) { - initializeCrossServerDirectMessage(velocityManager, directMessageHandler, languageManager) - } + val crossServerDirectMessage = + if (configuration.features.velocityIntegration.enabled && + configuration.features.velocityIntegration.crossServerDirectMessage && + velocityManager != null + ) { + initializeCrossServerDirectMessage(velocityManager, directMessageHandler, languageManager) + } else { + null + } return ServiceContainer( languageManager = languageManager, playerSettingsManager = playerSettingsManager, directMessageHandler = directMessageHandler, romajiConverter = romajiConverter, - channelManager = channelManager, - channelMembershipManager = channelMembershipManager, - channelMessageHandler = channelMessageHandler, - channelNotificationHandler = channelNotificationHandler, + conversionCache = japaneseConversion?.second, + channelManager = channelComponents?.channelManager, + channelMembershipManager = channelComponents?.channelMembershipManager, + channelMessageLogger = channelComponents?.channelMessageLogger, + channelMessageHandler = channelComponents?.channelMessageHandler, + channelNotificationHandler = channelComponents?.channelNotificationHandler, velocityConnectionManager = velocityManager, crossServerChatManager = crossServerManager, - crossServerDirectMessageManager = crossServerDirectMessageManager, - remotePlayerRegistry = remotePlayerRegistry, + crossServerDirectMessageManager = crossServerDirectMessage?.first, + remotePlayerRegistry = crossServerDirectMessage?.second, ) } @@ -169,7 +161,7 @@ class ServiceInitializer( val storage = YamlPlayerSettingsStorage( settingsFile = settingsFile, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) @@ -188,17 +180,16 @@ class ServiceInitializer( * - Google IME API client * - Romanji converter */ - private fun initializeJapaneseConversion(): RomanjiConverter { + private fun initializeJapaneseConversion(): Pair<RomanjiConverter, ConversionCache> { // Initialize conversion cache val cache = ConversionCache( cacheFile = plugin.dataFolder.resolve(configuration.features.japaneseConversion.cacheFilePath).toPath(), maxEntries = configuration.features.japaneseConversion.cacheMaxEntries, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) cache.loadFromDisk() - conversionCache = cache // Initialize Google IME API client val apiClient = @@ -217,7 +208,7 @@ class ServiceInitializer( ) logger.info("Japanese conversion feature enabled.") - return converter + return converter to cache } /** @@ -242,7 +233,6 @@ class ServiceInitializer( config = configuration.features.channelChat, ) manager.initialize() - channelManager = manager val membershipManager = ChannelMembershipManager( @@ -250,7 +240,6 @@ class ServiceInitializer( logger = logger, config = configuration.features.channelChat, ) - channelMembershipManager = membershipManager // Initialize channel message logger if enabled val messageLogger = @@ -265,7 +254,6 @@ class ServiceInitializer( maxFileSizeBytes = configuration.features.channelChat.messageLogging.maxFileSizeMB * 1024L * 1024L, retentionDays = configuration.features.channelChat.messageLogging.retentionDays, ).also { - channelMessageLogger = it logger.info( "Channel message logging enabled (retention: ${configuration.features.channelChat.messageLogging.retentionDays} days)", ) @@ -285,14 +273,12 @@ class ServiceInitializer( io.ktor.util.logging .KtorSimpleLogger("ChannelMessageHandler"), ) - channelMessageHandler = messageHandler val notificationHandler = ChannelNotificationHandler( channelManager = manager, languageManager = languageManager, ) - channelNotificationHandler = notificationHandler logger.info( "Channel manager, membership manager, " + @@ -303,6 +289,7 @@ class ServiceInitializer( channelMembershipManager = membershipManager, channelMessageHandler = messageHandler, channelNotificationHandler = notificationHandler, + channelMessageLogger = messageLogger, ) } @@ -318,7 +305,6 @@ class ServiceInitializer( logger = logger, ) manager.initialize() - velocityConnectionManager = manager // Register listener for first player join plugin.server.pluginManager.registerEvents( @@ -360,7 +346,6 @@ class ServiceInitializer( configuration = configuration, cacheSize = configuration.features.velocityIntegration.messageDeduplicationCacheSize, ) - crossServerChatManager = manager // Set the manager in VelocityConnectionManager to handle incoming messages velocityManager.setCrossServerChatManager(manager) @@ -380,9 +365,8 @@ class ServiceInitializer( velocityManager: VelocityConnectionManager, directMessageHandler: DirectMessageHandler, languageManager: LanguageManager, - ) { + ): Pair<CrossServerDirectMessageManager, RemotePlayerRegistry> { val registry = RemotePlayerRegistry(configuration.features.velocityIntegration.serverName) - remotePlayerRegistry = registry directMessageHandler.remotePlayerRegistry = registry val manager = @@ -394,11 +378,11 @@ class ServiceInitializer( languageManager = languageManager, cacheSize = configuration.features.velocityIntegration.messageDeduplicationCacheSize, ) - crossServerDirectMessageManager = manager velocityManager.setCrossServerDirectMessageManager(manager, registry) logger.info("Cross-server direct messages initialized") + return manager to registry } /** @@ -432,15 +416,16 @@ class ServiceInitializer( * Schedules periodic tasks such as cache saving. * Uses Folia-compatible AsyncScheduler API. */ - fun schedulePeriodicTasks() { - if (configuration.features.japaneseConversion.enabled && conversionCache != null) { + fun schedulePeriodicTasks(services: ServiceContainer) { + val conversionCache = services.conversionCache + if (conversionCache != null) { val intervalSeconds = configuration.features.japaneseConversion .cacheSaveIntervalSeconds .toLong() plugin.server.asyncScheduler.runAtFixedRate( plugin, - { conversionCache?.saveToDisk() }, + { conversionCache.saveToDisk() }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS, @@ -453,9 +438,9 @@ class ServiceInitializer( */ fun shutdown(services: ServiceContainer) { services.playerSettingsManager.saveToDisk() - conversionCache?.saveToDisk() + services.conversionCache?.saveToDisk() services.channelManager?.saveToDisk() - channelMessageLogger?.shutdown() + services.channelMessageLogger?.shutdown() services.velocityConnectionManager?.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 7305266..47780fb 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 @@ -446,7 +446,6 @@ class ChannelManager( } return ChannelContext( - channelId = channelId, channel = channel, members = members, ) @@ -480,7 +479,6 @@ class ChannelManager( saveToStorage() return ChannelContext( - channelId = channelId, channel = channel, members = members, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt index 07ce839..3833376 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt @@ -2,21 +2,38 @@ package dev.m1sk9.lunaticChat.paper.chat.channel import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassBanException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerMembershipLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException import dev.m1sk9.lunaticChat.engine.exception.ChannelRuntimeException +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig +import org.bukkit.Bukkit import java.util.UUID import java.util.logging.Logger +/** + * Whether the player is currently online and holds the moderation bypass permission. + * Offline players cannot be checked, matching how the permission is evaluated elsewhere. + */ +private fun hasChannelBypassPermission(playerId: UUID): Boolean = + Bukkit.getPlayer(playerId)?.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode) == true + +/** + * @param hasModerationBypass Whether a player is exempt from being kicked or banned. Injected + * rather than read from Bukkit inline so this manager stays testable without a running server. + */ class ChannelMembershipManager( private val channelManager: ChannelManager, private val logger: Logger, private val config: ChannelChatFeatureConfig, + private val hasModerationBypass: (UUID) -> Boolean = ::hasChannelBypassPermission, ) { /** * Checks if a player is a member of a channel. @@ -264,6 +281,58 @@ class ChannelMembershipManager( } /** + * Bans [targetId] from [channelId]. + * + * The bypass rule lives here rather than in the ban command so that every path to a ban - + * commands today, anything else later - is held to it. + * + * @return Result indicating success or failure. + * @throws ChannelPlayerBypassBanException if the target is exempt from moderation. + */ + fun banPlayer( + targetId: UUID, + channelId: String, + ): Result<Unit> { + if (hasModerationBypass(targetId)) { + return Result.failure(ChannelPlayerBypassBanException(targetId, channelId)) + } + return channelManager.banPlayer(channelId, targetId).map { } + } + + /** + * Removes [targetId] from [channelId] as a moderation action. + * + * @return Result indicating success or failure. + * @throws ChannelPlayerBypassKickException if the target is exempt from moderation. + */ + fun kickPlayer( + targetId: UUID, + channelId: String, + ): Result<Unit> { + if (hasModerationBypass(targetId)) { + return Result.failure(ChannelPlayerBypassKickException(targetId, channelId)) + } + return channelManager.removeMember(channelId, targetId) + } + + /** + * Adds [targetId] to [channelId] on [actorId]'s invitation, bypassing the private-channel gate. + * + * @return Result indicating success or failure. + * @throws ChannelCannotInviteSelfException if the actor invited themselves. + */ + fun inviteToChannel( + actorId: UUID, + targetId: UUID, + channelId: String, + ): Result<Unit> { + if (actorId == targetId) { + return Result.failure(ChannelCannotInviteSelfException(actorId)) + } + return joinChannel(targetId, channelId, bypassPrivateCheck = true) + } + + /** * Gets all channels where the player is a member. * * @param playerId The UUID of the player. 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 800e78a..985f321 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 @@ -8,6 +8,7 @@ import dev.m1sk9.lunaticChat.paper.common.playChannelReceiveNotification import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.withChatPlaceholders import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import io.ktor.util.logging.Logger import net.kyori.adventure.text.Component @@ -98,10 +99,11 @@ class ChannelMessageHandler( ): Component { val format = configuration.messageFormat.channelMessageFormat val text = - format - .replace("{sender}", senderName) - .replace("{channel}", channelName) - .replace("{message}", message) + format.withChatPlaceholders( + "sender" to senderName, + "channel" to channelName, + "message" to message, + ) return Component.text(text) } 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 f92f01d..c3fc923 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 @@ -7,6 +7,7 @@ import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.converter.convertWithRomaji import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.withChatPlaceholders import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import dev.m1sk9.lunaticChat.paper.velocity.RemotePlayerRegistry import net.kyori.adventure.text.Component @@ -236,10 +237,11 @@ class DirectMessageHandler( replyTo: String, ): Component { val text = - format - .replace("{sender}", senderName) - .replace("{recipient}", recipientName) - .replace("{message}", message) + format.withChatPlaceholders( + "sender" to senderName, + "recipient" to recipientName, + "message" to message, + ) return Component .text(text) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt index 53f29f4..c423d0c 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt @@ -2,7 +2,6 @@ package dev.m1sk9.lunaticChat.paper.command.core import io.papermc.paper.command.brigadier.CommandSourceStack import net.kyori.adventure.text.Component -import net.kyori.adventure.text.event.ClickEvent import org.bukkit.command.CommandSender import org.bukkit.entity.Player @@ -41,28 +40,4 @@ class CommandContext( fun reply(message: Component) { sender.sendMessage(message) } - - /** - * Sends a message with a click event to the command sender. - * - * @param message The message component to send - * @param event The click event to attach to the message - */ - fun replyWithEvent( - message: Component, - event: ClickEvent<*>, - ) { - message - .clickEvent(event) - .let { sender.sendMessage(it) } - } - - /** - * Sends a plain text message to the command sender. - * - * @param message The plain text message to send - */ - fun replyPlain(message: String) { - sender.sendPlainMessage(message) - } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt index 429a9c9..3bbd4a1 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt @@ -1,28 +1,20 @@ package dev.m1sk9.lunaticChat.paper.command.core import com.mojang.brigadier.builder.LiteralArgumentBuilder -import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.command.annotation.Command import dev.m1sk9.lunaticChat.paper.command.annotation.Permission -import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly -import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack -import io.papermc.paper.command.brigadier.Commands -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import kotlin.reflect.full.findAnnotation -import kotlin.reflect.full.memberFunctions /** - * Abstract base class for all LunaticChat commands. - * Provides common functionality and enforces consistent command structure. + * A command registered with the server under its own name. + * + * Subcommands attached under a parent literal extend [LunaticSubCommand] instead: they have no + * `@Command` annotation, so [name], [aliases] and [description] would have nothing to read. */ abstract class LunaticCommand( - // Reference to the main plugin instance - // DO NOT REMOVE - needed for command registration - protected val plugin: LunaticChat, -) { + plugin: LunaticChat, +) : LunaticCommandBase(plugin) { private val commandAnnotation: Command by lazy { this::class.annotations.filterIsInstance<Command>().firstOrNull() ?: throw IllegalStateException("Command class must be annotated with @Command") @@ -32,14 +24,6 @@ abstract class LunaticCommand( this::class.annotations.filterIsInstance<Permission>().firstOrNull() } - private val deprecatedAnnotation: Deprecated? by lazy { - this::class.annotations.filterIsInstance<Deprecated>().firstOrNull() - } - - private val isPlayerOnly: Boolean by lazy { - this::class.annotations.any { it is PlayerOnly } - } - /** The primary command name */ val name: String get() = commandAnnotation.name @@ -65,20 +49,6 @@ abstract class LunaticCommand( * Called by CommandRegistry during registration. */ fun buildWithChecks(): LiteralArgumentBuilder<CommandSourceStack> { - // If command is deprecated, replace with error message handler - deprecatedAnnotation?.let { deprecated -> - return Commands - .literal(name) - .executes { ctx -> - val context = wrapContext(ctx) - val result = - CommandResult.Failure( - MessageFormatter.formatError(deprecated.message), - ) - handleResult(context, result) - } - } - var builder = buildCommand() permission?.let { perm -> builder = @@ -89,92 +59,4 @@ abstract class LunaticCommand( return builder } - - /** - * Helper method for checking player-only restriction. - * Called at the beginning of execute methods. - */ - protected fun checkPlayerOnly(ctx: CommandContext): CommandResult? { - if (isPlayerOnly && !ctx.isPlayer) { - return CommandResult.Failure( - MessageFormatter.formatError( - plugin.languageManager.getMessage("general.playerOnlyCommand"), - ), - ) - } - - return null - } - - /** - * Utility to wrap Brigadier context into LunaticChat CommandContext. - */ - protected fun wrapContext(ctx: com.mojang.brigadier.context.CommandContext<CommandSourceStack>): CommandContext = - CommandContext(ctx.source) - - /** - * Helper for handling command results and sending appropriate messages. - */ - protected fun handleResult( - ctx: CommandContext, - result: CommandResult, - ): Int { - when (result) { - is CommandResult.Success -> {} - is CommandResult.SuccessWithMessage -> ctx.reply(result.message) - is CommandResult.Failure -> ctx.reply(result.message) - is CommandResult.InvalidUsage -> - ctx.reply( - Component - .text("Usage: ${result.usageHint}") - .color(NamedTextColor.RED), - ) - } - return result.toBrigadierResult() - } - - /** - * Creates alias nodes for a subcommand. - * Each alias gets the same children, executor, and permission requirement as the primary node. - * Brigadier automatically provides tab completion for all registered literal nodes. - * - * @param primary The primary subcommand builder - * @param aliases The alias names for the subcommand - * @return A list containing the primary builder followed by alias builders - */ - protected fun withAliases( - primary: LiteralArgumentBuilder<CommandSourceStack>, - aliases: List<String>, - ): List<LiteralArgumentBuilder<CommandSourceStack>> { - if (aliases.isEmpty()) return listOf(primary) - return listOf(primary) + - aliases.map { alias -> - val aliasBuilder = Commands.literal(alias) - primary.arguments.forEach { aliasBuilder.then(it) } - primary.command?.let { aliasBuilder.executes(it) } - aliasBuilder.requires(primary.requirement) - aliasBuilder - } - } - - /** - * Applies permission checks to a subcommand builder based on method-level @Permission annotation. - * Used for subcommands that use build() instead of buildCommand(). - * - * @param methodName The name of the method to check for @Permission annotation - * @param builder The subcommand builder to wrap - * @return The builder with permission checks applied if annotation is present - */ - protected fun applyMethodPermission( - methodName: String, - builder: LiteralArgumentBuilder<CommandSourceStack>, - ): LiteralArgumentBuilder<CommandSourceStack> { - val method = this::class.memberFunctions.find { it.name == methodName } ?: return builder - val permissionAnnotation = method.findAnnotation<Permission>() ?: return builder - val permissionNode = permissionAnnotation.value.objectInstance?.permissionNode ?: return builder - - return builder.requires { source -> - source.sender.hasPermission(permissionNode) - } - } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommandBase.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommandBase.kt new file mode 100644 index 0000000..89b9acc --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommandBase.kt @@ -0,0 +1,108 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +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 net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor + +/** + * Functionality shared by every command node, whether it is registered with the server + * ([LunaticCommand]) or attached under a parent literal ([LunaticSubCommand]). + */ +abstract class LunaticCommandBase( + // Reference to the main plugin instance + // DO NOT REMOVE - needed for command registration + protected val plugin: LunaticChat, +) { + private val isPlayerOnly: Boolean by lazy { + this::class.annotations.any { it is PlayerOnly } + } + + /** Source of localized text for [fail] and [ok]. */ + protected open val languageManager: LanguageManager get() = plugin.languageManager + + /** + * A failed result carrying the localized message at [key], formatted as an error. + */ + protected fun fail( + key: String, + args: Map<String, String> = emptyMap(), + ): CommandResult = CommandResult.Failure(MessageFormatter.formatError(languageManager.getMessage(key, args))) + + /** + * A successful result carrying the localized message at [key]. + */ + protected fun ok( + key: String, + args: Map<String, String> = emptyMap(), + ): CommandResult = CommandResult.SuccessWithMessage(MessageFormatter.format(languageManager.getMessage(key, args))) + + /** + * Helper method for checking player-only restriction. + * Called at the beginning of execute methods. + */ + protected fun checkPlayerOnly(ctx: CommandContext): CommandResult? { + if (isPlayerOnly && !ctx.isPlayer) { + return fail("general.playerOnlyCommand") + } + + return null + } + + /** + * Utility to wrap Brigadier context into LunaticChat CommandContext. + */ + protected fun wrapContext(ctx: com.mojang.brigadier.context.CommandContext<CommandSourceStack>): CommandContext = + CommandContext(ctx.source) + + /** + * Helper for handling command results and sending appropriate messages. + */ + protected fun handleResult( + ctx: CommandContext, + result: CommandResult, + ): Int { + when (result) { + is CommandResult.Success -> {} + is CommandResult.SuccessWithMessage -> ctx.reply(result.message) + is CommandResult.Failure -> ctx.reply(result.message) + is CommandResult.InvalidUsage -> + ctx.reply( + Component + .text("Usage: ${result.usageHint}") + .color(NamedTextColor.RED), + ) + } + return result.toBrigadierResult() + } + + /** + * Creates alias nodes for a subcommand. + * Each alias gets the same children, executor, and permission requirement as the primary node. + * Brigadier automatically provides tab completion for all registered literal nodes. + * + * @param primary The primary subcommand builder + * @param aliases The alias names for the subcommand + * @return A list containing the primary builder followed by alias builders + */ + protected fun withAliases( + primary: LiteralArgumentBuilder<CommandSourceStack>, + aliases: List<String>, + ): List<LiteralArgumentBuilder<CommandSourceStack>> { + if (aliases.isEmpty()) return listOf(primary) + return listOf(primary) + + aliases.map { alias -> + val aliasBuilder = Commands.literal(alias) + primary.arguments.forEach { aliasBuilder.then(it) } + primary.command?.let { aliasBuilder.executes(it) } + aliasBuilder.requires(primary.requirement) + aliasBuilder + } + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommand.kt new file mode 100644 index 0000000..e8db3fb --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommand.kt @@ -0,0 +1,44 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import dev.m1sk9.lunaticChat.paper.LunaticChat +import io.papermc.paper.command.brigadier.CommandSourceStack + +/** + * A command node attached under a parent literal rather than registered with the server. + * + * The permission gate is a declared property instead of an annotation read by reflection, so a + * subcommand that forgets it fails to compile rather than silently accepting everyone. + */ +abstract class LunaticSubCommand( + plugin: LunaticChat, +) : LunaticCommandBase(plugin) { + /** + * The literal this subcommand is typed as. Parents use it to look up help text, so it must be + * the same string [build] passes to `Commands.literal`. + */ + abstract val literal: String + + /** Permission required to see and run this subcommand, or null to inherit the parent's gate. */ + protected abstract val permissionNode: LunaticChatPermissionNode? + + /** Extra literals registered next to the primary node, sharing its arguments and executor. */ + protected open val aliases: List<String> = emptyList() + + /** + * Builds the primary subcommand node, without the permission gate. + */ + abstract fun build(): LiteralArgumentBuilder<CommandSourceStack> + + /** + * Builds the primary node and its aliases, each gated on [permissionNode]. + */ + fun buildAll(): List<LiteralArgumentBuilder<CommandSourceStack>> { + val primary = build() + permissionNode?.let { node -> + primary.requires { source -> source.sender.hasPermission(node.permissionNode) } + } + return withAliases(primary, aliases) + } +} 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 1e24430..86cac38 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 @@ -13,7 +13,6 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager -import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import dev.m1sk9.lunaticChat.paper.velocity.CrossServerDirectMessageManager import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands @@ -29,7 +28,7 @@ import org.bukkit.Bukkit class ReplyCommand( plugin: LunaticChat, private val dmHandler: DirectMessageHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, private val crossServerDirectMessageManager: CrossServerDirectMessageManager? = null, ) : LunaticCommand(plugin) { override val description: String @@ -59,32 +58,20 @@ class ReplyCommand( val sender = ctx.requirePlayer() val target = dmHandler.getReplyTarget(sender) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.replyTargetNotFound"), - ), - ) + ?: return fail("directMessage.replyTargetNotFound") return when (target) { is ReplyTarget.Local -> { val recipient = Bukkit.getPlayer(target.uuid) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.replyTargetNotFound"), - ), - ) + ?: return fail("directMessage.replyTargetNotFound") dmHandler.sendDirectMessage(sender, recipient, message) CommandResult.Success } is ReplyTarget.Remote -> { val manager = crossServerDirectMessageManager - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.replyTargetNotFound"), - ), - ) + ?: return fail("directMessage.replyTargetNotFound") manager.sendCrossServerMessage(sender, target.playerName, target.serverName, message) 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 ed2312d..990399e 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 @@ -14,7 +14,6 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager -import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import dev.m1sk9.lunaticChat.paper.velocity.CrossServerDirectMessageManager import dev.m1sk9.lunaticChat.paper.velocity.RemotePlayerRegistry import io.papermc.paper.command.brigadier.CommandSourceStack @@ -34,7 +33,7 @@ import com.mojang.brigadier.context.CommandContext as BrigadierCommandContext class TellCommand( plugin: LunaticChat, private val directMessageHandler: DirectMessageHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, private val crossServerDirectMessageManager: CrossServerDirectMessageManager? = null, private val remotePlayerRegistry: RemotePlayerRegistry? = null, private val localServerName: String = "", @@ -73,11 +72,7 @@ class TellCommand( val targetName = parts[0] val message = parts.getOrNull(1) if (targetName.isEmpty() || message.isNullOrBlank()) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.usage"), - ), - ) + return fail("directMessage.usage") } return execute(ctx, targetName, message) } @@ -93,28 +88,16 @@ class TellCommand( if (targetName.contains('@')) { val manager = crossServerDirectMessageManager - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.crossServerDisabled"), - ), - ) + ?: return fail("directMessage.crossServerDisabled") val name = targetName.substringBefore('@') val server = targetName.substringAfter('@') if (name.isEmpty() || server.isEmpty()) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.targetOffline", mapOf("target" to targetName)), - ), - ) + return fail("directMessage.targetOffline", mapOf("target" to targetName)) } if (name.equals(sender.name, ignoreCase = true) && server.equals(localServerName, ignoreCase = true) ) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.yourself"), - ), - ) + return fail("directMessage.yourself") } manager.sendCrossServerMessage(sender, name, server, message) return CommandResult.Success @@ -122,18 +105,10 @@ class TellCommand( val recipient = Bukkit.getPlayerExact(targetName) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.targetOffline", mapOf("target" to targetName)), - ), - ) + ?: return fail("directMessage.targetOffline", mapOf("target" to targetName)) if (recipient.uniqueId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("directMessage.yourself"), - ), - ) + return fail("directMessage.yourself") } directMessageHandler.sendDirectMessage(sender, recipient, message) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt index 13c30dd..e40ff3b 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt @@ -7,10 +7,9 @@ import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelBanCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelCreateCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelDeleteCommand @@ -37,118 +36,18 @@ class ChannelCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("ch")) - - @Permission(LunaticChatPermissionNode.Channel::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> { - val channelCommand = Commands.literal("channel") - - // Add subcommands (with aliases) - ChannelCreateCommand( - plugin, - channelManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelListCommand( - plugin, - channelManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelJoinCommand( - plugin, - channelManager, - membershipManager, - notificationHandler, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelLeaveCommand( - plugin, - channelManager, - membershipManager, - notificationHandler, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelSwitchCommand( - plugin, - channelManager, - membershipManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "channel" + override val permissionNode = LunaticChatPermissionNode.Channel + override val aliases = listOf("ch") - ChannelStatusCommand( - plugin, - channelManager, - membershipManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } + override fun build(): LiteralArgumentBuilder<CommandSourceStack> { + val channelCommand = Commands.literal(literal) - ChannelInfoCommand( - plugin, - channelManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelDeleteCommand( - plugin, - channelManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelInviteCommand( - plugin, - channelManager, - membershipManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelKickCommand( - plugin, - channelManager, - membershipManager, - notificationHandler, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelBanCommand( - plugin, - channelManager, - membershipManager, - notificationHandler, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelUnbanCommand( - plugin, - channelManager, - membershipManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelModCommand( - plugin, - channelManager, - membershipManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } - - ChannelOwnershipCommand( - plugin, - channelManager, - membershipManager, - languageManager, - ).buildAllWithPermissionCheck().forEach { channelCommand.then(it) } + subcommands().forEach { subcommand -> + subcommand.buildAll().forEach { channelCommand.then(it) } + } // Default help message when no subcommand is provided channelCommand.executes { ctx -> @@ -162,6 +61,30 @@ class ChannelCommand( return channelCommand } + /** + * The subcommands of /lc channel, in the order they are advertised by [showHelp]. + * + * Registration and help share this one list so a new subcommand cannot appear in the tree + * while staying invisible in the help output, or the reverse. + */ + private fun subcommands(): List<LunaticSubCommand> = + listOf( + ChannelCreateCommand(plugin, channelManager, languageManager), + ChannelListCommand(plugin, channelManager, languageManager), + ChannelJoinCommand(plugin, channelManager, membershipManager, notificationHandler, languageManager), + ChannelLeaveCommand(plugin, channelManager, membershipManager, notificationHandler, languageManager), + ChannelSwitchCommand(plugin, channelManager, membershipManager, languageManager), + ChannelStatusCommand(plugin, channelManager, membershipManager, languageManager), + ChannelInfoCommand(plugin, channelManager, languageManager), + ChannelDeleteCommand(plugin, channelManager, languageManager), + ChannelInviteCommand(plugin, channelManager, membershipManager, languageManager), + ChannelKickCommand(plugin, channelManager, membershipManager, notificationHandler, languageManager), + ChannelBanCommand(plugin, channelManager, membershipManager, notificationHandler, languageManager), + ChannelUnbanCommand(plugin, channelManager, membershipManager, languageManager), + ChannelModCommand(plugin, channelManager, membershipManager, languageManager), + ChannelOwnershipCommand(plugin, channelManager, membershipManager, languageManager), + ) + private fun showHelp(ctx: CommandContext): CommandResult { val sender = ctx.requirePlayer() @@ -170,138 +93,18 @@ class ChannelCommand( languageManager.getMessage("channel.help.header"), ), ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.create"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.list"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.join"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.leave"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.switch"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.status"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.info"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.delete"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.invite"), + subcommands().forEach { subcommand -> + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.${subcommand.literal}"), + ), ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.kick"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.ban"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.unban"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.mod"), - ), - ), - ) - sender.sendMessage( - Component - .text(" ") - .append( - MessageFormatter.formatSuccess( - languageManager.getMessage("channel.help.ownership"), - ), - ), - ) + ) + } return CommandResult.Success } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "Should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt index 2be0128..5b2ff6e 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt @@ -27,7 +27,7 @@ import io.papermc.paper.command.brigadier.Commands class LunaticChatCommand( plugin: LunaticChat, private val settingHandlerRegistry: SettingHandlerRegistry, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, private val configuration: LunaticChatConfiguration, ) : LunaticCommand(plugin) { override val description: String @@ -40,13 +40,13 @@ class LunaticChatCommand( plugin, settingHandlerRegistry, languageManager, - ).buildAllWithPermissionCheck().forEach { command.then(it) } + ).buildAll().forEach { command.then(it) } StatusCommand( plugin, languageManager, configuration, - ).buildAllWithPermissionCheck().forEach { command.then(it) } + ).buildAll().forEach { command.then(it) } // Add channel command if channel manager is available plugin.channelManager?.let { manager -> @@ -58,7 +58,7 @@ class LunaticChatCommand( membershipManager, notificationHandler, languageManager, - ).buildAllWithPermissionCheck().forEach { command.then(it) } + ).buildAll().forEach { command.then(it) } } } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt index bf5a245..a7afbf5 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt @@ -4,10 +4,9 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandlerRegistry import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager @@ -28,19 +27,11 @@ import io.papermc.paper.command.brigadier.Commands class SettingsCommand( plugin: LunaticChat, private val settingHandlerRegistry: SettingHandlerRegistry, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - /** - * Builds the setting subcommand structure with permission checks. - * This method should be called from parent commands. - */ - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("set")) + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "settings" + override val permissionNode = LunaticChatPermissionNode.Settings + override val aliases = listOf("set") /** * Builds the setting subcommand structure. @@ -49,9 +40,8 @@ class SettingsCommand( * - /lc setting <key> off * - /lc setting <key> (shows status) */ - @Permission(LunaticChatPermissionNode.Settings::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> { - val settingCommand = Commands.literal("settings") + override fun build(): LiteralArgumentBuilder<CommandSourceStack> { + val settingCommand = Commands.literal(literal) for (settingKey in SettingKey.values()) { val handler = settingHandlerRegistry.getHandler(settingKey) @@ -114,9 +104,4 @@ class SettingsCommand( ctx.reply(helpMessage) return CommandResult.Success } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "SettingsSubcommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt index 1f75cf2..62dc9cf 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt @@ -5,9 +5,8 @@ import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.BuildInfo import dev.m1sk9.lunaticChat.paper.LunaticChat -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter @@ -21,20 +20,15 @@ import net.kyori.adventure.text.format.NamedTextColor class StatusCommand( plugin: LunaticChat, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, private val configuration: LunaticChatConfiguration, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("st")) +) : LunaticSubCommand(plugin) { + override val literal = "status" + override val permissionNode = LunaticChatPermissionNode.Status + override val aliases = listOf("st") - @Permission(LunaticChatPermissionNode.Status::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = - Commands.literal("status").executes { ctx -> + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands.literal(literal).executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -170,16 +164,11 @@ class StatusCommand( label: String, enabled: Boolean, ): Component { - val toggleText = languageManager.getMessage(if (enabled) "toggle.on" else "toggle.off") + val toggleText = languageManager.getToggleText(enabled) val color = if (enabled) NamedTextColor.GREEN else NamedTextColor.GRAY return Component .text(" • ", NamedTextColor.GRAY) .append(Component.text("$label: ", NamedTextColor.GRAY)) .append(Component.text(toggleText, color)) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "Should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt index 67a4a5d..a57c92d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt @@ -4,7 +4,6 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassBanException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode @@ -12,10 +11,8 @@ import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -25,23 +22,17 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelBanCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), emptyList()) + override val languageManager: LanguageManager, +) : ChannelSubCommand(plugin, channelManager, membershipManager) { + override val literal = "ban" + override val permissionNode = LunaticChatPermissionNode.ChannelBan - @Permission(LunaticChatPermissionNode.ChannelBan::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("ban") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -77,137 +68,37 @@ class ChannelBanCommand( ): CommandResult { val sender = ctx.requirePlayer() - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.noActiveChannel"), - ), - ) - - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.noPermission"), - ), - ) - } - - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ban.playerNotFound", - mapOf("player" to playerName), - ), - ), - ) - } + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - val targetPlayerId = targetPlayer.uniqueId + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotBanSelf") - // Check if banning self - if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.cannotBanSelf"), - ), - ) - } + val targetName = target.name ?: playerName - // Check if target has bypass permission - val onlineTargetPlayer = Bukkit.getPlayer(playerName) - if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ban.cannotBanBypass", - mapOf("player" to onlineTargetPlayer.name), - ), - ), - ) - } - - // Ban player from channel - val banResult = channelManager.banPlayer(channelId, targetPlayerId) - return banResult.fold( + return membershipManager.banPlayer(target.uniqueId, channelId).fold( onSuccess = { - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId - - // Send notification to banned player if online - onlineTargetPlayer?.let { player -> - player.sendMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.ban.wasBanned", - mapOf("channel" to channelName, "banner" to sender.name), - ), - ), - ) - } + val channelName = channelNameOf(channelId) - // Broadcast ban notification to remaining members - notificationHandler.broadcastBan(channelId, playerName, sender.name) - - CommandResult.SuccessWithMessage( + Bukkit.getPlayer(playerName)?.sendMessage( MessageFormatter.format( languageManager.getMessage( - "channel.ban.success", - mapOf("player" to playerName, "channel" to channelName), + "channel.ban.wasBanned", + mapOf("channel" to channelName, "banner" to sender.name), ), ), ) + notificationHandler.broadcastBan(channelId, playerName, sender.name) + + okHere("success", mapOf("player" to playerName, "channel" to channelName)) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.error"), - ), - ) - } - is ChannelPlayerBypassBanException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ban.cannotBanBypass", - mapOf("player" to playerName), - ), - ), - ) - } - is ChannelPlayerAlreadyBannedException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ban.alreadyBanned", - mapOf("player" to playerName), - ), - ), - ) - } - else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.error"), - ), - ) - } + is ChannelPlayerBypassBanException -> failHere("cannotBanBypass", mapOf("player" to targetName)) + is ChannelPlayerAlreadyBannedException -> failHere("alreadyBanned", mapOf("player" to playerName)) + else -> failHere("error") } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelBanCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt index 8f93261..d1db42d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt @@ -9,10 +9,9 @@ import dev.m1sk9.lunaticChat.engine.exception.ChannelLimitExceededException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -22,20 +21,15 @@ import io.papermc.paper.command.brigadier.Commands class ChannelCreateCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("new")) + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "create" + override val permissionNode = LunaticChatPermissionNode.ChannelCreate + override val aliases = listOf("new") - @Permission(LunaticChatPermissionNode.ChannelCreate::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("create") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -95,13 +89,9 @@ class ChannelCreateCommand( // Validate channel ID pattern if (!channelId.matches(Channel.CHANNEL_ID_PATTERN)) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.create.invalidId", - mapOf("id" to channelId), - ), - ), + return fail( + "channel.create.invalidId", + mapOf("id" to channelId), ) } @@ -136,31 +126,13 @@ class ChannelCreateCommand( ) }, onFailure = { error -> - val messageKey = - when (error) { - is ChannelLimitExceededException -> - "channel.create.limitExceeded" - else -> - "channel.create.alreadyExists" - } - val params = - when (error) { - is ChannelLimitExceededException -> - mapOf("limit" to error.limit.toString()) - else -> - mapOf("id" to channelId) - } - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage(messageKey, params), - ), - ) + when (error) { + is ChannelLimitExceededException -> + fail("channel.create.limitExceeded", mapOf("limit" to error.limit.toString())) + else -> + fail("channel.create.alreadyExists", mapOf("id" to channelId)) + } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelCreateCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt index 9632d30..457151d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt @@ -8,12 +8,10 @@ import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand 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 @@ -21,20 +19,15 @@ import io.papermc.paper.command.brigadier.Commands class ChannelDeleteCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("del")) + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "delete" + override val permissionNode = LunaticChatPermissionNode.ChannelDelete + override val aliases = listOf("del") - @Permission(LunaticChatPermissionNode.ChannelDelete::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("delete") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -85,48 +78,27 @@ class ChannelDeleteCommand( val result = channelManager.deleteChannel(channelId, sender.uniqueId, hasBypass) return result.fold( onSuccess = { - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.delete.success", - mapOf("id" to channelId), - ), - ), + ok( + "channel.delete.success", + mapOf("id" to channelId), ) }, onFailure = { error -> when (error) { is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.delete.notFound", - mapOf("id" to channelId), - ), - ), + fail( + "channel.delete.notFound", + mapOf("id" to channelId), ) } is ChannelNoOwnerPermissionException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.delete.noPermission"), - ), - ) + fail("channel.delete.noPermission") } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.delete.error"), - ), - ) + fail("channel.delete.error") } } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelDeleteCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt index 212f3dc..01e7053 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt @@ -6,10 +6,9 @@ import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -22,24 +21,15 @@ import org.bukkit.Bukkit class ChannelInfoCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - companion object { - private const val MAX_MEMBERS_DISPLAY = 10 - } - - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "info" + override val permissionNode = LunaticChatPermissionNode.ChannelInfo + override val aliases = listOf("i") - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("i")) - - @Permission(LunaticChatPermissionNode.ChannelInfo::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("info") + .literal(literal) .executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -75,33 +65,21 @@ class ChannelInfoCommand( // Determine which channel to show info for val channelId = channelIdArg ?: channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.info.noActiveChannel"), - ), - ) + ?: return fail("channel.info.noActiveChannel") // Get channel val channel = channelManager.getChannel(channelId).getOrElse { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.info.notFound", - mapOf("channelId" to channelId), - ), - ), + return fail( + "channel.info.notFound", + mapOf("channelId" to channelId), ) } // Get members val members = channelManager.getChannelMembers(channelId).getOrElse { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.info.error"), - ), - ) + return fail("channel.info.error") } // Get owner name @@ -147,36 +125,10 @@ class ChannelInfoCommand( Bukkit.getOfflinePlayer(member.playerId).name } - val membersText = - if (memberNames.size > MAX_MEMBERS_DISPLAY) { - val displayNames = memberNames.take(MAX_MEMBERS_DISPLAY) - val message = - languageManager.getMessage( - "channel.info.membersOmitted", - mapOf("count" to memberNames.size.toString()), - ) - Component - .text(" ") - .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) - .append(Component.text(": ", NamedTextColor.GRAY)) - .append(Component.text(displayNames.joinToString(", "), NamedTextColor.WHITE)) - .append(Component.text(" ... ", NamedTextColor.GRAY)) - .append(Component.text("($message)", NamedTextColor.YELLOW)) - } else { - Component - .text(" ") - .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) - .append(Component.text(": ", NamedTextColor.GRAY)) - .append(Component.text(memberNames.joinToString(", "), NamedTextColor.WHITE)) - } + val membersText = memberListLine(memberNames, indent = " ", languageManager = languageManager) sender.sendMessage(membersText) return CommandResult.Success } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelInfoCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt index b6c0de3..03fb738 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt @@ -4,17 +4,15 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberLimitExceededException -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -24,22 +22,17 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelInviteCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("inv")) + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, + override val languageManager: LanguageManager, +) : ChannelSubCommand(plugin, channelManager, membershipManager) { + override val literal = "invite" + override val permissionNode = LunaticChatPermissionNode.ChannelInvite + override val aliases = listOf("inv") - @Permission(LunaticChatPermissionNode.ChannelInvite::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("invite") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -67,66 +60,17 @@ class ChannelInviteCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.noActiveChannel"), - ), - ) - - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.noPermission"), - ), - ) - } + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Find target player val targetPlayer = Bukkit.getPlayer(playerName) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.invite.playerNotFound", - mapOf("player" to playerName), - ), - ), - ) - - // Check if inviting self - if (targetPlayer.uniqueId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.cannotInviteSelf"), - ), - ) - } - - // Check if player is banned - val isBanned = channelManager.isPlayerBanned(channelId, targetPlayer.uniqueId).getOrElse { false } - if (isBanned) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.invite.playerBanned", - mapOf("player" to targetPlayer.name), - ), - ), - ) - } + ?: return failHere("playerNotFound", mapOf("player" to playerName)) - // Attempt to join the target player to the channel (bypass private check for invites) - val result = membershipManager.joinChannel(targetPlayer.uniqueId, channelId, bypassPrivateCheck = true) + val result = membershipManager.inviteToChannel(sender.uniqueId, targetPlayer.uniqueId, channelId) return result.fold( onSuccess = { - // Send success message to sender - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId + val channelName = channelNameOf(channelId) // Send notification to invited player targetPlayer.sendMessage( @@ -138,58 +82,16 @@ class ChannelInviteCommand( ), ) - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.invite.success", - mapOf("player" to targetPlayer.name, "channel" to channelName), - ), - ), - ) + okHere("success", mapOf("player" to targetPlayer.name, "channel" to channelName)) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.error"), - ), - ) - } - is ChannelMemberLimitExceededException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.invite.channelFull", - mapOf("limit" to error.limit.toString()), - ), - ), - ) - } - is ChannelPlayerBannedException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.invite.playerBanned", - mapOf("player" to targetPlayer.name), - ), - ), - ) - } - else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.error"), - ), - ) - } + is ChannelCannotInviteSelfException -> failHere("cannotInviteSelf") + is ChannelMemberLimitExceededException -> failHere("channelFull", mapOf("limit" to error.limit.toString())) + is ChannelPlayerBannedException -> failHere("playerBanned", mapOf("player" to targetPlayer.name)) + else -> failHere("error") } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelInviteCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt index e5433eb..9b154b3 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt @@ -15,13 +15,11 @@ import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.common.playChannelJoinNotification 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 @@ -31,20 +29,15 @@ class ChannelJoinCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("j")) + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "join" + override val permissionNode = LunaticChatPermissionNode.ChannelJoin + override val aliases = listOf("j") - @Permission(LunaticChatPermissionNode.ChannelJoin::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("join") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -82,97 +75,56 @@ class ChannelJoinCommand( // Broadcast join notification to all channel members notificationHandler.broadcastJoin(channelId, sender.name) - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.join.success", - mapOf("channelName" to (channel?.name ?: channelId), "channelId" to channelId), - ), - ), + ok( + "channel.join.success", + mapOf("channelName" to (channel?.name ?: channelId), "channelId" to channelId), ) }, onFailure = { error -> when (error) { is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.join.notFound", - mapOf("channelId" to channelId), - ), - ), + fail( + "channel.join.notFound", + mapOf("channelId" to channelId), ) } is ChannelAlreadyActiveException -> { val channel = channelManager.getChannel(channelId).getOrNull() - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.join.alreadyActive", - mapOf("channelName" to (channel?.name ?: channelId)), - ), - ), + fail( + "channel.join.alreadyActive", + mapOf("channelName" to (channel?.name ?: channelId)), ) } is ChannelMemberAlreadyException -> { val channel = channelManager.getChannel(channelId).getOrNull() - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.join.alreadyMember", - mapOf("channelName" to (channel?.name ?: channelId)), - ), - ), + fail( + "channel.join.alreadyMember", + mapOf("channelName" to (channel?.name ?: channelId)), ) } is ChannelMemberLimitExceededException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.join.channelMemberLimitExceeded", - mapOf("limit" to error.limit.toString()), - ), - ), + fail( + "channel.join.channelMemberLimitExceeded", + mapOf("limit" to error.limit.toString()), ) } is ChannelPlayerMembershipLimitExceededException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.join.playerChannelLimitExceeded", - mapOf("limit" to error.limit.toString()), - ), - ), + fail( + "channel.join.playerChannelLimitExceeded", + mapOf("limit" to error.limit.toString()), ) } is ChannelPlayerBannedException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.join.playerBanned"), - ), - ) + fail("channel.join.playerBanned") } is ChannelPrivateRequiresInvitationException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.join.privateChannel"), - ), - ) + fail("channel.join.privateChannel") } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.join.error"), - ), - ) + fail("channel.join.error") } } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelJoinCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt index d4dd103..fa1c4d7 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt @@ -4,16 +4,14 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -23,23 +21,18 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelKickCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("k")) + override val languageManager: LanguageManager, +) : ChannelSubCommand(plugin, channelManager, membershipManager) { + override val literal = "kick" + override val permissionNode = LunaticChatPermissionNode.ChannelKick + override val aliases = listOf("k") - @Permission(LunaticChatPermissionNode.ChannelKick::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("kick") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -75,136 +68,46 @@ class ChannelKickCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.noActiveChannel"), - ), - ) + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.noPermission"), - ), - ) - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotKickSelf") - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) + val targetId = target.uniqueId + val targetName = target.name ?: playerName - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.kick.playerNotFound", - mapOf("player" to playerName), - ), - ), - ) + if (!membershipManager.isMember(targetId, channelId).getOrElse { false }) { + return failHere("notMember", mapOf("player" to playerName)) } - val targetPlayerId = targetPlayer.uniqueId - - // Check if kicking self - if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.cannotKickSelf"), - ), - ) - } - - // Check if target has bypass permission - val onlineTargetPlayer = Bukkit.getPlayer(playerName) - if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.kick.cannotKickBypass", - mapOf("player" to onlineTargetPlayer.name), - ), - ), - ) - } - - // Check if target is a member - val isMember = membershipManager.isMember(targetPlayerId, channelId).getOrElse { false } - if (!isMember) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.kick.notMember", - mapOf("player" to playerName), - ), - ), - ) - } - - // Remove from channel - val removeResult = channelManager.removeMember(channelId, targetPlayerId) - return removeResult.fold( + return membershipManager.kickPlayer(targetId, channelId).fold( onSuccess = { // Clear their active channel if this was it - if (channelManager.getPlayerChannel(targetPlayerId) == channelId) { - channelManager.setPlayerChannel(targetPlayerId, null) - } - - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId - - // Send notification to kicked player if online - onlineTargetPlayer?.let { player -> - player.sendMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.kick.wasKicked", - mapOf("channel" to channelName, "kicker" to sender.name), - ), - ), - ) + if (channelManager.getPlayerChannel(targetId) == channelId) { + channelManager.setPlayerChannel(targetId, null) } - // Broadcast kick notification to remaining members - notificationHandler.broadcastKick(channelId, playerName, sender.name) + val channelName = channelNameOf(channelId) - CommandResult.SuccessWithMessage( + Bukkit.getPlayer(playerName)?.sendMessage( MessageFormatter.format( languageManager.getMessage( - "channel.kick.success", - mapOf("player" to playerName, "channel" to channelName), + "channel.kick.wasKicked", + mapOf("channel" to channelName, "kicker" to sender.name), ), ), ) + notificationHandler.broadcastKick(channelId, playerName, sender.name) + + okHere("success", mapOf("player" to playerName, "channel" to channelName)) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.error"), - ), - ) - } - else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.error"), - ), - ) - } + is ChannelPlayerBypassKickException -> failHere("cannotKickBypass", mapOf("player" to targetName)) + else -> failHere("error") } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelKickCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt index 47556bf..b90895f 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt @@ -8,12 +8,10 @@ import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand 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 @@ -23,19 +21,14 @@ class ChannelLeaveCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("l")) + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "leave" + override val permissionNode = LunaticChatPermissionNode.ChannelLeave + override val aliases = listOf("l") - @Permission(LunaticChatPermissionNode.ChannelLeave::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = - Commands.literal("leave").executes { ctx -> + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands.literal(literal).executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -58,38 +51,21 @@ class ChannelLeaveCommand( notificationHandler.broadcastLeave(currentChannelId, sender.name) } - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.leave.success", - mapOf("channelName" to (currentChannel?.name ?: currentChannelId ?: "Unknown")), - ), - ), + ok( + "channel.leave.success", + mapOf("channelName" to (currentChannel?.name ?: currentChannelId ?: "Unknown")), ) }, onFailure = { error -> when (error) { is ChannelNotMemberException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.leave.noActiveChannel"), - ), - ) + fail("channel.leave.noActiveChannel") } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.leave.error"), - ), - ) + fail("channel.leave.error") } } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelLeaveCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt index daa060b..92a0815 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt @@ -6,10 +6,9 @@ import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -24,24 +23,19 @@ import kotlin.math.ceil class ChannelListCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { companion object { private const val CHANNELS_PER_PAGE = 10 } - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("ls")) + override val literal = "list" + override val permissionNode = LunaticChatPermissionNode.ChannelList + override val aliases = listOf("ls") - @Permission(LunaticChatPermissionNode.ChannelList::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("list") + .literal(literal) .executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -189,17 +183,8 @@ class ChannelListCommand( CommandResult.Success }, onFailure = { error -> - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.list.error"), - ), - ) + fail("channel.list.error") }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelListCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt index 9a29c75..baf9d5c 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt @@ -4,15 +4,12 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -22,22 +19,16 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelModCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), emptyList()) - - @Permission(LunaticChatPermissionNode.ChannelMod::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, + override val languageManager: LanguageManager, +) : ChannelSubCommand(plugin, channelManager, membershipManager) { + override val literal = "mod" + override val permissionNode = LunaticChatPermissionNode.ChannelMod + + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("mod") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -73,63 +64,16 @@ class ChannelModCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.noActiveChannel"), - ), - ) - - // Check if sender is OWNER - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole != ChannelRole.OWNER) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.noPermission"), - ), - ) - } - - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.mod.playerNotFound", - mapOf("player" to playerName), - ), - ), - ) - } - - val targetPlayerId = targetPlayer.uniqueId + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.OWNER)?.let { return it } - // Check if modding self - if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.cannotModSelf"), - ), - ) - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotModSelf") - // Check if target is a member - val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) - if (targetRole == null) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.mod.notMember", - mapOf("player" to playerName), - ), - ), - ) - } + val targetPlayerId = target.uniqueId + val targetRole = + membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) + ?: return failHere("notMember", mapOf("player" to playerName)) // Toggle mod status val newRole = @@ -164,38 +108,14 @@ class ChannelModCommand( ) } - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.mod.success", - mapOf("player" to playerName, "action" to action, "channel" to channelName), - ), - ), + ok( + "channel.mod.success", + mapOf("player" to playerName, "action" to action, "channel" to channelName), ) }, onFailure = { error -> - when (error) { - is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.error"), - ), - ) - } - else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.error"), - ), - ) - } - } + fail("channel.mod.error") }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelModCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt index 59ee5c0..374684b 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt @@ -4,15 +4,12 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -22,22 +19,17 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelOwnershipCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("own")) - - @Permission(LunaticChatPermissionNode.ChannelOwnership::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, + override val languageManager: LanguageManager, +) : ChannelSubCommand(plugin, channelManager, membershipManager) { + override val literal = "ownership" + override val permissionNode = LunaticChatPermissionNode.ChannelOwnership + override val aliases = listOf("own") + + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("ownership") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -73,62 +65,15 @@ class ChannelOwnershipCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.noActiveChannel"), - ), - ) + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.OWNER)?.let { return it } - // Check if sender is OWNER - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole != ChannelRole.OWNER) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.noPermission"), - ), - ) - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotTransferToSelf") - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ownership.playerNotFound", - mapOf("player" to playerName), - ), - ), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if transferring to self - if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.cannotTransferToSelf"), - ), - ) - } - - // Check if target is a member - val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) - if (targetRole == null) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ownership.notMember", - mapOf("player" to playerName), - ), - ), - ) + val targetPlayerId = target.uniqueId + if (membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) == null) { + return failHere("notMember", mapOf("player" to playerName)) } // Transfer ownership @@ -150,38 +95,14 @@ class ChannelOwnershipCommand( ) } - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.ownership.success", - mapOf("player" to playerName, "channel" to channelName), - ), - ), + ok( + "channel.ownership.success", + mapOf("player" to playerName, "channel" to channelName), ) }, onFailure = { error -> - when (error) { - is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.error"), - ), - ) - } - else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.error"), - ), - ) - } - } + fail("channel.ownership.error") }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelOwnershipCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt index 8a4400b..78871b7 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt @@ -7,10 +7,9 @@ import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -27,23 +26,14 @@ class ChannelStatusCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - companion object { - private const val MAX_MEMBERS_DISPLAY = 10 - } - - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("st")) - - @Permission(LunaticChatPermissionNode.ChannelStatus::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = - Commands.literal("status").executes { ctx -> + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "status" + override val permissionNode = LunaticChatPermissionNode.ChannelStatus + override val aliases = listOf("st") + + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands.literal(literal).executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -61,11 +51,7 @@ class ChannelStatusCommand( // Get all player's channels val playerChannelIds = membershipManager.getPlayerChannels(sender.uniqueId).getOrElse { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.status.error"), - ), - ) + return fail("channel.status.error") } // Display header @@ -114,36 +100,7 @@ class ChannelStatusCommand( playerName + roleText } - val membersText = - if (memberNames.size > MAX_MEMBERS_DISPLAY) { - val displayNames = memberNames.take(MAX_MEMBERS_DISPLAY) - val message = - languageManager.getMessage( - "channel.info.membersOmitted", - mapOf("count" to memberNames.size.toString()), - ) - Component - .text(" ") - .append( - Component.text( - languageManager.getMessage("channel.info.members"), - NamedTextColor.GRAY, - ), - ).append(Component.text(": ", NamedTextColor.GRAY)) - .append(Component.text(displayNames.joinToString(", "), NamedTextColor.WHITE)) - .append(Component.text(" ... ", NamedTextColor.GRAY)) - .append(Component.text("($message)", NamedTextColor.YELLOW)) - } else { - Component - .text(" ") - .append( - Component.text( - languageManager.getMessage("channel.info.members"), - NamedTextColor.GRAY, - ), - ).append(Component.text(": ", NamedTextColor.GRAY)) - .append(Component.text(memberNames.joinToString(", "), NamedTextColor.WHITE)) - } + val membersText = memberListLine(memberNames, indent = " ", languageManager = languageManager) sender.sendMessage(membersText) } @@ -247,9 +204,4 @@ class ChannelStatusCommand( return CommandResult.Success } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelStatusCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt new file mode 100644 index 0000000..b48c879 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt @@ -0,0 +1,97 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole +import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import org.bukkit.entity.Player +import java.util.UUID + +private const val MAX_MEMBERS_DISPLAY = 10 + +/** + * Renders a channel's member list on one line, truncated to [MAX_MEMBERS_DISPLAY] names with a + * count of what was left out. + * + * @param indent Leading whitespace, which differs by how deeply the caller nests the line + */ +internal fun memberListLine( + memberNames: List<String>, + indent: String, + languageManager: LanguageManager, +): Component { + val shown = memberNames.take(MAX_MEMBERS_DISPLAY) + val line = + Component + .text(indent) + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(shown.joinToString(", "), NamedTextColor.WHITE)) + + if (memberNames.size <= MAX_MEMBERS_DISPLAY) return line + + val omitted = + languageManager.getMessage( + "channel.info.membersOmitted", + mapOf("count" to memberNames.size.toString()), + ) + return line + .append(Component.text(" ... ", NamedTextColor.GRAY)) + .append(Component.text("($omitted)", NamedTextColor.YELLOW)) +} + +/** + * A subcommand of `/lc channel`. + * + * The moderation subcommands all open with the same sequence - resolve the sender's active + * channel, check their role, resolve the named target - once written out in full in each of them. + * These helpers name each step, and derive their message keys from [literal] so a subcommand + * cannot accidentally report another one's text. + */ +abstract class ChannelSubCommand( + plugin: LunaticChat, + protected val channelManager: ChannelManager, + protected val membershipManager: ChannelMembershipManager, +) : LunaticSubCommand(plugin) { + /** A failure carrying `channel.<literal>.<suffix>`. */ + protected fun failHere( + suffix: String, + args: Map<String, String> = emptyMap(), + ): CommandResult = fail("channel.$literal.$suffix", args) + + /** A success carrying `channel.<literal>.<suffix>`. */ + protected fun okHere( + suffix: String, + args: Map<String, String> = emptyMap(), + ): CommandResult = ok("channel.$literal.$suffix", args) + + /** The channel [sender] is currently talking in, or null if they have none. */ + protected fun activeChannelOf(sender: Player): String? = channelManager.getPlayerChannel(sender.uniqueId) + + /** The display name of [channelId], falling back to the id when the channel is gone. */ + protected fun channelNameOf(channelId: String): String = channelManager.getChannel(channelId).getOrNull()?.name ?: channelId + + /** + * Null when [playerId] holds [role] or higher in [channelId]; otherwise the "no permission" + * failure for this subcommand. + */ + protected fun denyUnlessRole( + playerId: UUID, + channelId: String, + role: ChannelRole, + ): CommandResult? = if (membershipManager.hasRole(playerId, channelId, role).getOrDefault(false)) null else failHere("noPermission") + + /** + * The named player if the server has ever seen them, or null. Offline players are resolvable + * because bans and role changes must work while the target is away. + */ + protected fun knownPlayer(playerName: String): OfflinePlayer? = + Bukkit.getOfflinePlayer(playerName).takeIf { it.hasPlayedBefore() || it.isOnline } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt index b5bd18d..2f71b7d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt @@ -10,12 +10,10 @@ import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand 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 @@ -24,20 +22,15 @@ class ChannelSwitchCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), listOf("sw")) + override val languageManager: LanguageManager, +) : LunaticSubCommand(plugin) { + override val literal = "switch" + override val permissionNode = LunaticChatPermissionNode.ChannelSwitch + override val aliases = listOf("sw") - @Permission(LunaticChatPermissionNode.ChannelSwitch::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("switch") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -72,62 +65,37 @@ class ChannelSwitchCommand( onSuccess = { val channel = channelManager.getChannel(channelId).getOrNull() - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.switch.success", - mapOf("channelName" to (channel?.name ?: channelId), "channelId" to channelId), - ), - ), + ok( + "channel.switch.success", + mapOf("channelName" to (channel?.name ?: channelId), "channelId" to channelId), ) }, onFailure = { error -> when (error) { is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.switch.notFound", - mapOf("channelId" to channelId), - ), - ), + fail( + "channel.switch.notFound", + mapOf("channelId" to channelId), ) } is ChannelAlreadyActiveException -> { val channel = channelManager.getChannel(channelId).getOrNull() - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.switch.alreadyActive", - mapOf("channelName" to (channel?.name ?: channelId)), - ), - ), + fail( + "channel.switch.alreadyActive", + mapOf("channelName" to (channel?.name ?: channelId)), ) } is ChannelNotMemberException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.switch.notMember", - mapOf("channelId" to channelId), - ), - ), + fail( + "channel.switch.notMember", + mapOf("channelId" to channelId), ) } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.switch.error"), - ), - ) + fail("channel.switch.error") } } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelSwitchCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt index 2bde39f..c361d62 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt @@ -4,18 +4,14 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand 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 org.bukkit.Bukkit @@ -23,22 +19,16 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelUnbanCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List<LiteralArgumentBuilder<CommandSourceStack>> = - withAliases(buildWithPermissionCheck(), emptyList()) + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, + override val languageManager: LanguageManager, +) : ChannelSubCommand(plugin, channelManager, membershipManager) { + override val literal = "unban" + override val permissionNode = LunaticChatPermissionNode.ChannelUnban - @Permission(LunaticChatPermissionNode.ChannelUnban::class) - fun build(): LiteralArgumentBuilder<CommandSourceStack> = + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands - .literal("unban") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -72,91 +62,21 @@ class ChannelUnbanCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.noActiveChannel"), - ), - ) - - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.noPermission"), - ), - ) - } - - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.unban.playerNotFound", - mapOf("player" to playerName), - ), - ), - ) - } - - val targetPlayerId = targetPlayer.uniqueId + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) - // Unban player from channel - val unbanResult = channelManager.unbanPlayer(channelId, targetPlayerId) - return unbanResult.fold( + return channelManager.unbanPlayer(channelId, target.uniqueId).fold( onSuccess = { - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId - - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.unban.success", - mapOf("player" to playerName, "channel" to channelName), - ), - ), - ) + okHere("success", mapOf("player" to playerName, "channel" to channelNameOf(channelId))) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.error"), - ), - ) - } - is ChannelPlayerNotBannedException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.unban.playerNotBanned", - mapOf("player" to playerName), - ), - ), - ) - } - else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.error"), - ), - ) - } + is ChannelPlayerNotBannedException -> failHere("playerNotBanned", mapOf("player" to playerName)) + else -> failHere("error") } }, ) } - - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = - throw UnsupportedOperationException( - "ChannelUnbanCommand should use build() method instead of buildCommand()", - ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt index 2c81197..cb7829b 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt @@ -32,7 +32,7 @@ import net.kyori.adventure.text.format.NamedTextColor class VelocityStatusCommand( plugin: LunaticChat, private val velocityConnectionManager: VelocityConnectionManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticCommand(plugin) { override val description: String get() = languageManager.getMessage("commandDescription.lcv") diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandler.kt index c36257c..7b1fcdd 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandler.kt @@ -2,18 +2,21 @@ package dev.m1sk9.lunaticChat.paper.command.setting import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager /** - * Interface for handling individual setting operations. - * Each setting (japanese, notice, etc.) implements this interface to provide - * its own logic for getting, setting, and displaying status. + * Reads and writes one player setting, identified by [key]. + * + * Everything that differs between settings lives on the [SettingKey]; this class is the shared + * mechanism that applies it. */ -interface SettingHandler { - /** - * The setting key this handler manages. - */ - val key: SettingKey - +class SettingHandler( + val key: SettingKey, + private val settingsManager: PlayerSettingsManager, + private val languageManager: LanguageManager, +) { /** * Enables or disables the setting for a player. * @@ -24,7 +27,14 @@ interface SettingHandler { fun execute( ctx: CommandContext, enable: Boolean, - ): CommandResult + ): CommandResult { + val player = ctx.requirePlayer() + val settings = settingsManager.getSettings(player.uniqueId) + settingsManager.updateSettings(key.write(settings, enable)) + + player.sendMessage(MessageFormatter.formatSuccess(message(key.toggleMessageKey, enable))) + return CommandResult.Success + } /** * Shows the current status of the setting for a player. @@ -32,5 +42,16 @@ interface SettingHandler { * @param ctx The command context containing player information * @return Command result indicating success or failure */ - fun showStatus(ctx: CommandContext): CommandResult + fun showStatus(ctx: CommandContext): CommandResult { + val player = ctx.requirePlayer() + val settings = settingsManager.getSettings(player.uniqueId) + + player.sendMessage(MessageFormatter.format(message(key.statusMessageKey, key.read(settings)))) + return CommandResult.Success + } + + private fun message( + messageKey: String, + enabled: Boolean, + ): String = languageManager.getMessage(messageKey, mapOf("toggle" to languageManager.getToggleText(enabled))) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt index 076a7aa..2106cc5 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt @@ -1,29 +1,68 @@ package dev.m1sk9.lunaticChat.paper.command.setting +import dev.m1sk9.lunaticChat.engine.settings.PlayerChatSettings + /** * Sealed class representing all available setting keys. - * Each setting key has a unique identifier used in command arguments. + * + * A setting is fully described here: the literal it is typed as, the messages that report it, and + * how it is read from and written to [PlayerChatSettings]. [SettingHandler] is the one mechanism + * that drives all of them. + * + * @property key Unique identifier used in command arguments + * @property toggleMessageKey Language key for the message confirming a change + * @property statusMessageKey Language key for the message reporting the current value */ sealed class SettingKey( val key: String, + val toggleMessageKey: String, + val statusMessageKey: String, ) { + abstract fun read(settings: PlayerChatSettings): Boolean + + abstract fun write( + settings: PlayerChatSettings, + enabled: Boolean, + ): PlayerChatSettings + /** * Japanese romaji conversion setting * Command: /lc setting japanese <on|off> */ - data object Japanese : SettingKey("japanese") + data object Japanese : SettingKey("japanese", "romajiConversion.toggle", "romajiConversion.status") { + override fun read(settings: PlayerChatSettings) = settings.japaneseConversionEnabled + + override fun write( + settings: PlayerChatSettings, + enabled: Boolean, + ) = settings.copy(japaneseConversionEnabled = enabled) + } /** * Direct message notification setting * Command: /lc setting notice <on|off> */ - data object Notice : SettingKey("notice") + data object Notice : SettingKey("notice", "directMessage.noticeToggle", "directMessage.noticeStatus") { + override fun read(settings: PlayerChatSettings) = settings.directMessageNotificationEnabled + + override fun write( + settings: PlayerChatSettings, + enabled: Boolean, + ) = settings.copy(directMessageNotificationEnabled = enabled) + } /** * Channel message notification setting * Command: /lc setting chNotice <on|off> */ - data object ChNotice : SettingKey("chNotice") + data object ChNotice : SettingKey("chNotice", "channelMessage.noticeToggle", "channelMessage.noticeStatus") { + override fun read(settings: PlayerChatSettings) = settings.channelMessageNotificationEnabled + + override fun write( + settings: PlayerChatSettings, + enabled: Boolean, + ) = settings.copy(channelMessageNotificationEnabled = enabled) + } companion object { /** diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt deleted file mode 100644 index 8867e7c..0000000 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.m1sk9.lunaticChat.paper.command.setting.handler - -import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandler -import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey -import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager -import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter -import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager - -/** - * Handles the channel message notification setting. - * Manages enabling/disabling channel message notifications for players. - */ -class ChannelMessageNoticeSettingHandler( - private val settingsManager: PlayerSettingsManager, - private val languageManager: LanguageManager, -) : SettingHandler { - override val key: SettingKey = SettingKey.ChNotice - - override fun execute( - ctx: CommandContext, - enable: Boolean, - ): CommandResult { - val player = ctx.requirePlayer() - val currentSettings = settingsManager.getSettings(player.uniqueId) - val updatedSettings = currentSettings.copy(channelMessageNotificationEnabled = enable) - settingsManager.updateSettings(updatedSettings) - - val toggleText = languageManager.getToggleText(enable) - val message = - MessageFormatter.formatSuccess( - languageManager.getMessage("channelMessage.noticeToggle", mapOf("toggle" to toggleText)), - ) - - player.sendMessage(message) - return CommandResult.Success - } - - override fun showStatus(ctx: CommandContext): CommandResult { - val player = ctx.requirePlayer() - val settings = settingsManager.getSettings(player.uniqueId) - - val toggleText = languageManager.getToggleText(settings.channelMessageNotificationEnabled) - val message = - MessageFormatter.format( - languageManager.getMessage("channelMessage.noticeStatus", mapOf("toggle" to toggleText)), - ) - - player.sendMessage(message) - return CommandResult.Success - } -} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt deleted file mode 100644 index 1564615..0000000 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.m1sk9.lunaticChat.paper.command.setting.handler - -import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandler -import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey -import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager -import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter -import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager - -/** - * Handles the direct message notification setting. - * Manages enabling/disabling DM notifications for players. - */ -class DirectMessageNoticeSettingHandler( - private val settingsManager: PlayerSettingsManager, - private val languageManager: LanguageManager, -) : SettingHandler { - override val key: SettingKey = SettingKey.Notice - - override fun execute( - ctx: CommandContext, - enable: Boolean, - ): CommandResult { - val player = ctx.requirePlayer() - val currentSettings = settingsManager.getSettings(player.uniqueId) - val updatedSettings = currentSettings.copy(directMessageNotificationEnabled = enable) - settingsManager.updateSettings(updatedSettings) - - val toggleText = languageManager.getToggleText(enable) - val message = - MessageFormatter.formatSuccess( - languageManager.getMessage("directMessage.noticeToggle", mapOf("toggle" to toggleText)), - ) - - player.sendMessage(message) - return CommandResult.Success - } - - override fun showStatus(ctx: CommandContext): CommandResult { - val player = ctx.requirePlayer() - val settings = settingsManager.getSettings(player.uniqueId) - - val toggleText = languageManager.getToggleText(settings.directMessageNotificationEnabled) - val message = - MessageFormatter.format( - languageManager.getMessage("directMessage.noticeStatus", mapOf("toggle" to toggleText)), - ) - - player.sendMessage(message) - return CommandResult.Success - } -} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt deleted file mode 100644 index 2c682ba..0000000 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.m1sk9.lunaticChat.paper.command.setting.handler - -import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandler -import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey -import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager -import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter -import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager - -/** - * Handles the Japanese romaji conversion setting. - * Manages enabling/disabling Japanese conversion for players. - */ -class JapaneseConversionSettingHandler( - private val settingsManager: PlayerSettingsManager, - private val languageManager: LanguageManager, -) : SettingHandler { - override val key: SettingKey = SettingKey.Japanese - - override fun execute( - ctx: CommandContext, - enable: Boolean, - ): CommandResult { - val player = ctx.requirePlayer() - val currentSettings = settingsManager.getSettings(player.uniqueId) - val updatedSettings = currentSettings.copy(japaneseConversionEnabled = enable) - settingsManager.updateSettings(updatedSettings) - - val toggleText = languageManager.getToggleText(enable) - val message = - MessageFormatter.formatSuccess( - languageManager.getMessage("romajiConversion.toggle", mapOf("toggle" to toggleText)), - ) - - player.sendMessage(message) - return CommandResult.Success - } - - override fun showStatus(ctx: CommandContext): CommandResult { - val player = ctx.requirePlayer() - val settings = settingsManager.getSettings(player.uniqueId) - - val toggleText = languageManager.getToggleText(settings.japaneseConversionEnabled) - val message = - MessageFormatter.format( - languageManager.getMessage("romajiConversion.status", mapOf("toggle" to toggleText)), - ) - - player.sendMessage(message) - return CommandResult.Success - } -} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/UpdateChecker.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/UpdateChecker.kt index 49d6852..56ceabf 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/UpdateChecker.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/UpdateChecker.kt @@ -14,12 +14,6 @@ import java.util.logging.Logger data class GitHubRelease( @SerialName("tag_name") val tagName: String, - @SerialName("name") - val name: String, - @SerialName("published_at") - val publishedAt: String, - @SerialName("html_url") - val htmlUrl: String, ) class UpdateChecker( 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 bab4e8d..ad0a9e0 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 @@ -1,12 +1,10 @@ package dev.m1sk9.lunaticChat.paper.converter import dev.m1sk9.lunaticChat.engine.converter.CacheData +import dev.m1sk9.lunaticChat.paper.DebouncedSaver import kotlinx.serialization.json.Json -import org.bukkit.plugin.java.JavaPlugin import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean import java.util.logging.Logger import kotlin.io.path.bufferedReader import kotlin.io.path.exists @@ -15,11 +13,10 @@ import kotlin.io.path.writeText class ConversionCache( private val cacheFile: Path, private val maxEntries: Int = 500, - private val plugin: JavaPlugin, + private val saver: DebouncedSaver, private val logger: Logger, ) { private val conversionMemoryCache = ConcurrentHashMap<String, String>() - private val conversionSaveQueue = AtomicBoolean(false) companion object { private const val CACHE_VERSION = "1" @@ -84,7 +81,7 @@ class ConversionCache( } conversionMemoryCache[key] = value - queueSaveToDisk() + saver.request(::saveToDisk) } /** @@ -108,20 +105,6 @@ class ConversionCache( } } - private fun queueSaveToDisk() { - if (conversionSaveQueue.compareAndSet(false, true)) { - plugin.server.asyncScheduler.runDelayed( - plugin, - { - conversionSaveQueue.set(false) - saveToDisk() - }, - 5, - TimeUnit.SECONDS, - ) - } - } - // 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. diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/ChatFormat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/ChatFormat.kt new file mode 100644 index 0000000..3397ee1 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/ChatFormat.kt @@ -0,0 +1,10 @@ +package dev.m1sk9.lunaticChat.paper.i18n + +/** + * Substitutes `{name}` placeholders in one of the configurable chat formats. + * + * Which names a format accepts is documented alongside it in config.yml; going through this + * function keeps every format applying them the same way. + */ +fun String.withChatPlaceholders(vararg values: Pair<String, String>): String = + values.fold(this) { text, (name, value) -> text.replace("{$name}", value) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt index e13ebc7..dd702ba 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/LanguageManager.kt @@ -1,27 +1,14 @@ package dev.m1sk9.lunaticChat.paper.i18n import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.YamlList +import com.charleskorn.kaml.YamlMap +import com.charleskorn.kaml.YamlNode +import com.charleskorn.kaml.YamlScalar import org.bukkit.plugin.java.JavaPlugin import java.util.logging.Logger /** - * Represents a value in a YAML structure. - */ -private sealed class YamlValue { - data class StringValue( - val value: String, - ) : YamlValue() - - data class MapValue( - val value: Map<String, YamlValue>, - ) : YamlValue() - - data class ListValue( - val value: List<YamlValue>, - ) : YamlValue() -} - -/** * Manages language files and provides message retrieval with string-based keys. * * This class handles: @@ -84,87 +71,34 @@ class LanguageManager( ?: throw IllegalStateException("Language file not found: $resourcePath") val yamlContent = stream.bufferedReader().use { it.readText() } - val yamlNode = Yaml.default.parseToYamlNode(yamlContent) + val root = Yaml.default.parseToYamlNode(yamlContent) + check(root is YamlMap) { "Root YAML node must be a map" } - val rootMap = yamlNodeToMap(yamlNode) - return flattenYaml(rootMap) + return buildMap { flattenInto(root, prefix = "", into = this) } } /** - * Converts a YamlNode to a type-safe YamlValue structure. - */ - private fun yamlNodeToMap(node: com.charleskorn.kaml.YamlNode): Map<String, YamlValue> = - when (node) { - is com.charleskorn.kaml.YamlMap -> { - val result = mutableMapOf<String, YamlValue>() - node.entries.forEach { entry -> - val key = entry.key.content - val value = yamlNodeToValue(entry.value) - result[key] = value - } - result - } - else -> throw IllegalStateException("Root YAML node must be a map") - } - - /** - * Converts a YamlNode to a type-safe YamlValue. - */ - private fun yamlNodeToValue(node: com.charleskorn.kaml.YamlNode): YamlValue = - when (node) { - is com.charleskorn.kaml.YamlMap -> { - val result = mutableMapOf<String, YamlValue>() - node.entries.forEach { entry -> - val key = entry.key.content - val value = yamlNodeToValue(entry.value) - result[key] = value - } - YamlValue.MapValue(result) - } - is com.charleskorn.kaml.YamlList -> { - YamlValue.ListValue(node.items.map { yamlNodeToValue(it) }) - } - is com.charleskorn.kaml.YamlScalar -> YamlValue.StringValue(node.content) - else -> YamlValue.StringValue(node.contentToString()) - } - - /** - * Flattens a nested map into dot-notation keys. + * Flattens a YAML tree into dot-notation keys. * Example: {"toggle": {"on": "有効"}} -> {"toggle.on": "有効"} */ - private fun flattenYaml( - map: Map<String, YamlValue>, - prefix: String = "", - ): Map<String, String> { - val result = mutableMapOf<String, String>() - - map.forEach { (key, value) -> - val fullKey = if (prefix.isEmpty()) key else "$prefix.$key" - - when (value) { - is YamlValue.MapValue -> { - result.putAll(flattenYaml(value.value, fullKey)) - } - is YamlValue.StringValue -> result[fullKey] = value.value - is YamlValue.ListValue -> { - // Lists are converted to comma-separated strings for simplicity - result[fullKey] = value.value.joinToString(", ") { yamlValueToString(it) } + private fun flattenInto( + node: YamlNode, + prefix: String, + into: MutableMap<String, String>, + ) { + when (node) { + is YamlMap -> + node.entries.forEach { (key, value) -> + val fullKey = if (prefix.isEmpty()) key.content else "$prefix.${key.content}" + flattenInto(value, fullKey, into) } - } + // Lists are converted to comma-separated strings for simplicity + is YamlList -> into[prefix] = node.items.joinToString(", ") { scalarText(it) } + else -> into[prefix] = scalarText(node) } - - return result } - /** - * Converts a YamlValue to String for flattening purposes. - */ - private fun yamlValueToString(value: YamlValue): String = - when (value) { - is YamlValue.StringValue -> value.value - is YamlValue.MapValue -> value.value.toString() - is YamlValue.ListValue -> value.value.toString() - } + private fun scalarText(node: YamlNode): String = (node as? YamlScalar)?.content ?: node.contentToString() /** * Retrieves a message for the given string key with optional placeholder substitution. diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt index d20c9f7..31aa0bb 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/MessageFormatter.kt @@ -30,21 +30,14 @@ object MessageFormatter { fun format( message: String, highlightPlaceholders: Boolean = true, - ): Component { - val prefix = - Component - .text("[LC] ") - .color(PREFIX_COLOR) - - val messageComponent = + ): Component = + prefixed( if (highlightPlaceholders) { formatWithPlaceholders(message, MESSAGE_COLOR) } else { Component.text(message).color(MESSAGE_COLOR) - } - - return prefix.append(messageComponent) - } + }, + ) /** * Formats an error message with the prefix and red text. @@ -52,16 +45,7 @@ object MessageFormatter { * @param message The error message text * @return A formatted Component with red text */ - fun formatError(message: String): Component { - val prefix = - Component - .text("[LC] ") - .color(PREFIX_COLOR) - - val messageComponent = formatWithPlaceholders(message, ERROR_COLOR) - - return prefix.append(messageComponent) - } + fun formatError(message: String): Component = prefixed(formatWithPlaceholders(message, ERROR_COLOR)) /** * Formats a success message with the prefix and green text. @@ -69,16 +53,9 @@ object MessageFormatter { * @param message The success message text * @return A formatted Component with green text */ - fun formatSuccess(message: String): Component { - val prefix = - Component - .text("[LC] ") - .color(PREFIX_COLOR) + fun formatSuccess(message: String): Component = prefixed(formatWithPlaceholders(message, SUCCESS_COLOR)) - val messageComponent = formatWithPlaceholders(message, SUCCESS_COLOR) - - return prefix.append(messageComponent) - } + private fun prefixed(message: Component): Component = Component.text("[LC] ").color(PREFIX_COLOR).append(message) /** * Parses a message and highlights placeholders in {braces} with yellow color. diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt index f0927d8..9ee8d5a 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt @@ -17,21 +17,32 @@ class PlayerSettingsManager( private val storage: YamlPlayerSettingsStorage, private val logger: Logger, ) { - private val japaneseConversionCache = ConcurrentHashMap<UUID, Boolean>() - private val directMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>() - private val channelMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>() - private lateinit var settingsData: PlayerSettingsData + private val settings = ConcurrentHashMap<UUID, PlayerChatSettings>() + + // Written back unchanged: nothing migrates on it yet, but rewriting the file must not + // silently relabel a schema this build does not understand. + private var schemaVersion = PlayerSettingsData().version /** * Initializes the settings manager by loading all settings from disk into memory. * This should be called once during plugin startup. */ fun initialize() { - settingsData = storage.loadFromDisk() - japaneseConversionCache.putAll(settingsData.japaneseConversion) - directMessageNotificationCache.putAll(settingsData.directMessageNotification) - channelMessageNotificationCache.putAll(settingsData.channelMessageNotification) - logger.info("Loaded settings for ${japaneseConversionCache.size} players") + val data = storage.loadFromDisk() + schemaVersion = data.version + val knownPlayers = + data.japaneseConversion.keys + data.directMessageNotification.keys + data.channelMessageNotification.keys + + knownPlayers.forEach { uuid -> + settings[uuid] = + PlayerChatSettings( + uuid = uuid, + japaneseConversionEnabled = data.japaneseConversion.getOrDefault(uuid, true), + directMessageNotificationEnabled = data.directMessageNotification.getOrDefault(uuid, true), + channelMessageNotificationEnabled = data.channelMessageNotification.getOrDefault(uuid, true), + ) + } + logger.info("Loaded settings for ${settings.size} players") } /** @@ -41,17 +52,7 @@ class PlayerSettingsManager( * @param uuid The UUID of the player * @return The player's settings */ - fun getSettings(uuid: UUID): PlayerChatSettings { - val japaneseConversionEnabled = japaneseConversionCache.getOrDefault(uuid, true) - val directMessageNotificationEnabled = directMessageNotificationCache.getOrDefault(uuid, true) - val channelMessageNotificationEnabled = channelMessageNotificationCache.getOrDefault(uuid, true) - return PlayerChatSettings( - uuid = uuid, - japaneseConversionEnabled = japaneseConversionEnabled, - directMessageNotificationEnabled = directMessageNotificationEnabled, - channelMessageNotificationEnabled = channelMessageNotificationEnabled, - ) - } + fun getSettings(uuid: UUID): PlayerChatSettings = settings[uuid] ?: PlayerChatSettings(uuid = uuid) /** * Updates player settings in cache and queues async save to disk. @@ -59,18 +60,8 @@ class PlayerSettingsManager( * @param settings The updated settings to save */ fun updateSettings(settings: PlayerChatSettings) { - japaneseConversionCache[settings.uuid] = settings.japaneseConversionEnabled - directMessageNotificationCache[settings.uuid] = settings.directMessageNotificationEnabled - channelMessageNotificationCache[settings.uuid] = settings.channelMessageNotificationEnabled - - settingsData = - settingsData.copy( - japaneseConversion = japaneseConversionCache.toMap(), - directMessageNotification = directMessageNotificationCache.toMap(), - channelMessageNotification = channelMessageNotificationCache.toMap(), - ) - - storage.queueAsyncSave(settingsData) + this.settings[settings.uuid] = settings + storage.queueAsyncSave(::snapshot) logger.fine("Updated settings for player ${settings.uuid}") } @@ -79,6 +70,14 @@ class PlayerSettingsManager( * This should only be called during plugin shutdown. */ fun saveToDisk() { - storage.saveToDisk(settingsData) + storage.saveToDisk(snapshot()) } + + private fun snapshot(): PlayerSettingsData = + PlayerSettingsData( + version = schemaVersion, + japaneseConversion = settings.mapValues { it.value.japaneseConversionEnabled }, + directMessageNotification = settings.mapValues { it.value.directMessageNotificationEnabled }, + channelMessageNotification = settings.mapValues { it.value.channelMessageNotificationEnabled }, + ) } 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 5a2c8e2..f4d8a6f 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 @@ -2,36 +2,31 @@ package dev.m1sk9.lunaticChat.paper.settings import com.charleskorn.kaml.Yaml import dev.m1sk9.lunaticChat.engine.settings.PlayerSettingsData -import org.bukkit.plugin.java.JavaPlugin +import dev.m1sk9.lunaticChat.paper.DebouncedSaver import java.nio.file.Path -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean import java.util.logging.Logger import kotlin.io.path.bufferedReader import kotlin.io.path.exists -import kotlin.io.path.listDirectoryEntries import kotlin.io.path.writeText /** * Handles YAML file I/O operations for player settings. - * Provides backup functionality and async save with debouncing. + * Provides async save with debouncing. * * @property settingsFile The path to the YAML settings file - * @property plugin The plugin instance for scheduling async tasks + * @property saver Coalesces bursts of save requests into one asynchronous write * @property logger The logger for logging operations */ class YamlPlayerSettingsStorage( private val settingsFile: Path, - private val plugin: JavaPlugin, + private val saver: DebouncedSaver, private val logger: Logger, ) { private val yaml = Yaml.default - private val saveFlag = AtomicBoolean(false) /** * Loads player settings from the YAML file. - * If the file doesn't exist, returns empty settings. - * If loading fails, attempts to restore from backup. + * If the file doesn't exist or cannot be parsed, returns empty settings. * * @return The loaded settings or empty settings if file doesn't exist */ @@ -46,18 +41,6 @@ class YamlPlayerSettingsStorage( yaml.decodeFromString(PlayerSettingsData.serializer(), yamlContent) } catch (e: Exception) { logger.severe("Failed to load settings file: ${e.message}") - - val backup = findLatestBackup() - if (backup != null) { - logger.warning("Attempting to restore from backup: $backup") - try { - val content = backup.bufferedReader().use { it.readText() } - return yaml.decodeFromString(PlayerSettingsData.serializer(), content) - } catch (backupError: Exception) { - logger.severe("Backup restoration failed: ${backupError.message}") - } - } - logger.warning("Using empty settings as fallback") PlayerSettingsData() } @@ -83,34 +66,11 @@ class YamlPlayerSettingsStorage( * Queues an async save operation with 5-second debouncing. * Multiple save requests within 5 seconds are batched into a single save. * - * @param data The settings data to save + * @param data Supplies the settings to write. It is called when the write runs rather than + * when it is queued, so the batched write persists every change made during the delay - not + * just the one that started it. */ - fun queueAsyncSave(data: PlayerSettingsData) { - if (saveFlag.compareAndSet(false, true)) { - plugin.server.asyncScheduler.runDelayed( - plugin, - { - saveFlag.set(false) - saveToDisk(data) - }, - 5, - TimeUnit.SECONDS, - ) - } + fun queueAsyncSave(data: () -> PlayerSettingsData) { + saver.request { saveToDisk(data()) } } - - /** - * Finds the most recent backup file. - * - * @return The path to the latest backup, or null if no backups exist - */ - private fun findLatestBackup(): Path? = - try { - settingsFile.parent - .listDirectoryEntries("player-settings.yaml.backup.*") - .maxByOrNull { it.fileName.toString() } - } catch (e: Exception) { - logger.warning("Failed to find backup: ${e.message}") - null - } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt index 53eced1..2f2bde0 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt @@ -1,12 +1,14 @@ package dev.m1sk9.lunaticChat.paper.velocity import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration +import dev.m1sk9.lunaticChat.paper.i18n.withChatPlaceholders import net.kyori.adventure.text.Component import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer import org.bukkit.plugin.Plugin import java.util.UUID -import java.util.concurrent.ConcurrentHashMap import java.util.logging.Level import java.util.logging.Logger @@ -24,15 +26,7 @@ class CrossServerChatManager( private val configuration: LunaticChatConfiguration, private val cacheSize: Int = 100, ) { - companion object { - private const val CLEANUP_THRESHOLD_MILLIS = 60_000L - } - - /** - * Cache of recently processed message IDs (messageId -> timestamp) - * Used for deduplication - */ - private val processedMessages = ConcurrentHashMap<String, Long>() + private val processedMessages = MessageDeduplicationCache(cacheSize, logger, "global chat") /** * Sends a global chat message to Velocity for cross-server broadcast @@ -51,7 +45,7 @@ class CrossServerChatManager( val serverName = configuration.features.velocityIntegration.serverName // Mark as processed immediately to prevent echo - processedMessages[messageId] = System.currentTimeMillis() + processedMessages.markProcessed(messageId) val globalChatMessage = PluginMessage.GlobalChatMessage( @@ -72,9 +66,8 @@ class CrossServerChatManager( if (player != null) { player.sendPluginMessage( plugin, - "lunaticchat:main", - dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec - .encode(globalChatMessage), + PluginMessageChannel.ID, + PluginMessageCodec.encode(globalChatMessage), ) logger.info("Sent global chat message to Velocity: messageId=$messageId, player=$playerName") } else { @@ -85,11 +78,6 @@ class CrossServerChatManager( } }, ) - - // Cleanup old messages if cache is too large - if (processedMessages.size > cacheSize) { - cleanupOldMessages() - } } catch (e: Exception) { logger.log(Level.SEVERE, "Failed to send global chat message", e) } @@ -103,13 +91,11 @@ class CrossServerChatManager( fun handleIncomingMessage(message: PluginMessage.GlobalChatMessage) { try { // Check if already processed (deduplication) - if (!shouldProcessMessage(message.messageId)) { + if (!processedMessages.isNew(message.messageId)) { logger.fine("Ignoring duplicate message: messageId=${message.messageId}") return } - - // Mark as processed - processedMessages[message.messageId] = System.currentTimeMillis() + processedMessages.markProcessed(message.messageId) // Broadcast to all players on this server val formattedMessage = formatCrossServerMessage(message) @@ -127,11 +113,6 @@ class CrossServerChatManager( "Broadcasted global chat message from ${message.serverName}: " + "player=${message.playerName}, messageId=${message.messageId}", ) - - // Cleanup if needed - if (processedMessages.size > cacheSize) { - cleanupOldMessages() - } } catch (e: Exception) { logger.log(Level.SEVERE, "Failed to handle incoming global chat message", e) } @@ -146,61 +127,12 @@ class CrossServerChatManager( private fun formatCrossServerMessage(message: PluginMessage.GlobalChatMessage): Component { val format = configuration.messageFormat.crossServerGlobalChatFormat val formattedText = - format - .replace("{server}", message.serverName) - .replace("{sender}", message.playerName) - .replace("{message}", message.message) + format.withChatPlaceholders( + "server" to message.serverName, + "sender" to message.playerName, + "message" to message.message, + ) return LegacyComponentSerializer.legacySection().deserialize(formattedText) } - - /** - * Checks if a message should be processed (not a duplicate) - * - * @param messageId Message ID to check - * @return true if message should be processed, false if it's a duplicate - */ - private fun shouldProcessMessage(messageId: String): Boolean = !processedMessages.containsKey(messageId) - - /** - * Removes old messages from the cache (LRU cleanup) - * Keeps only the most recent messages - */ - private fun cleanupOldMessages() { - try { - val currentTime = System.currentTimeMillis() - val cutoffTime = currentTime - CLEANUP_THRESHOLD_MILLIS - - // Collect keys to remove (ConcurrentHashMap iterator doesn't support remove()) - val keysToRemove = mutableListOf<String>() - processedMessages.entries.forEach { entry -> - if (entry.value < cutoffTime) { - keysToRemove.add(entry.key) - } - } - - // Remove expired entries - keysToRemove.forEach { key -> - processedMessages.remove(key) - } - var removedCount = keysToRemove.size - - // If still over cache size, remove oldest entries - if (processedMessages.size > cacheSize) { - val sortedEntries = processedMessages.entries.sortedBy { it.value } - val toRemove = processedMessages.size - cacheSize - - sortedEntries.take(toRemove).forEach { entry -> - processedMessages.remove(entry.key) - removedCount++ - } - } - - if (removedCount > 0) { - logger.fine("Cleaned up $removedCount old messages from deduplication cache") - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Failed to cleanup old messages", e) - } - } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt index b8390a7..b35f6a5 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt @@ -1,6 +1,7 @@ package dev.m1sk9.lunaticChat.paper.velocity import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration @@ -9,7 +10,6 @@ import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import org.bukkit.entity.Player import org.bukkit.plugin.Plugin import java.util.UUID -import java.util.concurrent.ConcurrentHashMap import java.util.logging.Level import java.util.logging.Logger @@ -30,12 +30,7 @@ class CrossServerDirectMessageManager( private val languageManager: LanguageManager, private val cacheSize: Int = 100, ) { - companion object { - private const val CHANNEL = "lunaticchat:main" - private const val CLEANUP_THRESHOLD_MILLIS = 60_000L - } - - private val processedMessages = ConcurrentHashMap<String, Long>() + private val processedMessages = MessageDeduplicationCache(cacheSize, logger, "direct message") /** * Sends a direct message to a player on another server through Velocity. @@ -52,7 +47,7 @@ class CrossServerDirectMessageManager( ) { try { val messageId = UUID.randomUUID().toString() - processedMessages[messageId] = System.currentTimeMillis() + processedMessages.markProcessed(messageId) val relayedMessage = directMessageHandler.handleOutgoingCrossServerMessage( @@ -73,15 +68,11 @@ class CrossServerDirectMessageManager( message = relayedMessage, ) - sender.sendPluginMessage(plugin, CHANNEL, PluginMessageCodec.encode(relay)) + sender.sendPluginMessage(plugin, PluginMessageChannel.ID, PluginMessageCodec.encode(relay)) logger.info( "Sent direct message to Velocity: messageId=$messageId, " + "target=$targetName@$targetServerName", ) - - if (processedMessages.size > cacheSize) { - cleanupOldMessages() - } } catch (e: Exception) { logger.log(Level.SEVERE, "Failed to send cross-server direct message", e) } @@ -92,11 +83,11 @@ class CrossServerDirectMessageManager( */ fun handleIncomingMessage(message: PluginMessage.DirectMessageRelay) { try { - if (!shouldProcessMessage(message.messageId)) { + if (!processedMessages.isNew(message.messageId)) { logger.fine("Ignoring duplicate direct message: messageId=${message.messageId}") return } - processedMessages[message.messageId] = System.currentTimeMillis() + processedMessages.markProcessed(message.messageId) plugin.server.scheduler.runTask( plugin, @@ -117,10 +108,6 @@ class CrossServerDirectMessageManager( ) }, ) - - if (processedMessages.size > cacheSize) { - cleanupOldMessages() - } } catch (e: Exception) { logger.log(Level.SEVERE, "Failed to handle incoming direct message", e) } @@ -153,33 +140,4 @@ class CrossServerDirectMessageManager( logger.log(Level.SEVERE, "Failed to handle direct message error", e) } } - - private fun shouldProcessMessage(messageId: String): Boolean = !processedMessages.containsKey(messageId) - - private fun cleanupOldMessages() { - try { - val cutoffTime = System.currentTimeMillis() - CLEANUP_THRESHOLD_MILLIS - - val keysToRemove = processedMessages.entries.filter { it.value < cutoffTime }.map { it.key } - keysToRemove.forEach { processedMessages.remove(it) } - var removedCount = keysToRemove.size - - if (processedMessages.size > cacheSize) { - val toRemove = processedMessages.size - cacheSize - processedMessages.entries - .sortedBy { it.value } - .take(toRemove) - .forEach { - processedMessages.remove(it.key) - removedCount++ - } - } - - if (removedCount > 0) { - logger.fine("Cleaned up $removedCount old messages from direct message dedup cache") - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Failed to cleanup old direct messages", e) - } - } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCache.kt new file mode 100644 index 0000000..e804e03 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCache.kt @@ -0,0 +1,70 @@ +package dev.m1sk9.lunaticChat.paper.velocity + +import java.util.concurrent.ConcurrentHashMap +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Remembers recently seen message IDs so a message relayed back to its origin server is dropped + * instead of echoed. + * + * Entries expire after [CLEANUP_THRESHOLD_MILLIS]; if the cache is still over [cacheSize] after + * that, the oldest entries go too. Entries are ordered by millisecond timestamp, so a burst of + * more than [cacheSize] messages inside one millisecond evicts arbitrarily among them. + * + * @param cacheSize Soft upper bound on retained entries + * @param logger Where cleanup failures are reported + * @param description Names this cache in log output + */ +class MessageDeduplicationCache( + private val cacheSize: Int, + private val logger: Logger, + private val description: String, +) { + companion object { + private const val CLEANUP_THRESHOLD_MILLIS = 60_000L + } + + private val processedMessages = ConcurrentHashMap<String, Long>() + + /** + * Records [messageId] as seen, evicting stale entries when the cache outgrows [cacheSize]. + */ + fun markProcessed(messageId: String) { + processedMessages[messageId] = System.currentTimeMillis() + if (processedMessages.size > cacheSize) { + evict() + } + } + + /** + * Returns true when [messageId] has not been seen yet. + */ + fun isNew(messageId: String): Boolean = !processedMessages.containsKey(messageId) + + private fun evict() { + try { + val cutoffTime = System.currentTimeMillis() - CLEANUP_THRESHOLD_MILLIS + + val expired = processedMessages.entries.filter { it.value < cutoffTime }.map { it.key } + expired.forEach { processedMessages.remove(it) } + var removedCount = expired.size + + if (processedMessages.size > cacheSize) { + processedMessages.entries + .sortedBy { it.value } + .take(processedMessages.size - cacheSize) + .forEach { + processedMessages.remove(it.key) + removedCount++ + } + } + + if (removedCount > 0) { + logger.fine("Cleaned up $removedCount old messages from $description dedup cache") + } + } catch (e: Exception) { + logger.log(Level.WARNING, "Failed to clean up $description dedup cache", e) + } + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt index 1f4ccb4..bdedb6f 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt @@ -1,6 +1,7 @@ package dev.m1sk9.lunaticChat.paper.velocity import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import dev.m1sk9.lunaticChat.engine.protocol.ProtocolVersion import org.bukkit.entity.Player @@ -21,7 +22,7 @@ class VelocityConnectionManager( private var remotePlayerRegistry: RemotePlayerRegistry? = null, ) : PluginMessageListener { companion object { - private const val CHANNEL = "lunaticchat:main" + private val CHANNEL = PluginMessageChannel.ID private const val HANDSHAKE_TIMEOUT_SECONDS = 5L } diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml index 5a76910..f65474a 100644 --- a/platform-paper/src/main/resources/languages/en.yml +++ b/platform-paper/src/main/resources/languages/en.yml @@ -12,9 +12,6 @@ # ---------------------------------------------- commandDescription: - jp: "Toggle romaji-to-kana conversion on/off" - notice: "Toggle direct message notifications on/off" - chNotice: "Toggle channel message notifications on/off" reply: "Reply to the player who last sent/received a direct message" tell: "Send a direct message to another player" lc: "LunaticChat Main Command" @@ -206,7 +203,6 @@ channel: general: playerOnlyCommand: "This command can only be executed by players." newUpdateAvailable: "The new version of LunaticChat is now available! You can download it from GitHub or Modrinth." - noPermission: "You do not have permission to execute this command." spyMessage: "You have been granted permission, so this message is displayed in spy mode." nightlyWarning: "You are running a nightly build. This build may be unstable or contain bugs." nightlyReportIssue: "If you encounter any issues, please report them on GitHub Issues." diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml index b2582c4..764951b 100644 --- a/platform-paper/src/main/resources/languages/ja.yml +++ b/platform-paper/src/main/resources/languages/ja.yml @@ -14,9 +14,6 @@ commandDescription: tell: "他のプレイヤーにダイレクトメッセージを送信します" reply: "最後にダイレクトメッセージを送信/受信したプレイヤーに返信します" - jp: "かな・ローマ字変換機能のオン/オフを切り替えます" - notice: "ダイレクトメッセージ通知のオン/オフを切り替えます" - chNotice: "チャンネルメッセージ通知のオン/オフを切り替えます" lc: "LunaticChat のメインコマンド" lcv: "LunaticChat Velocity 連携コマンド" @@ -206,7 +203,6 @@ channel: general: playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます" newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます" - noPermission: "このコマンドを実行する権限がありません" spyMessage: "あなたに権限が付与されているため、このメッセージはスパイ状態で表示されています" nightlyWarning: "ナイトリービルドを使用しています。このビルドは不安定であったり、バグが含まれている可能性があります。" nightlyReportIssue: "問題を発見した場合は、GitHub Issues で報告してください。" diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt index 1fcc39b..28e1c82 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt @@ -3,10 +3,13 @@ package dev.m1sk9.lunaticChat.paper.chat.channel import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassBanException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerMembershipLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException import dev.m1sk9.lunaticChat.paper.TestUtils @@ -15,6 +18,7 @@ import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig import io.mockk.every import io.mockk.mockk +import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -28,6 +32,7 @@ class ChannelMembershipManagerTest { maxChannelsPerServer: Int = 10, maxMembersPerChannel: Int = 50, maxMembershipPerPlayer: Int = 5, + playersWithBypass: Set<UUID> = emptySet(), ): Triple<ChannelMembershipManager, ChannelManager, TestUtils.TestLogger> { val logger = TestUtils.TestLogger() val storage = mockk<ChannelStorage>(relaxed = true) @@ -44,7 +49,8 @@ class ChannelMembershipManagerTest { val channelManager = ChannelManager(storage, logger, config) channelManager.initialize() - val membershipManager = ChannelMembershipManager(channelManager, logger, config) + val membershipManager = + ChannelMembershipManager(channelManager, logger, config, hasModerationBypass = { it in playersWithBypass }) return Triple(membershipManager, channelManager, logger) } @@ -381,4 +387,89 @@ class ChannelMembershipManagerTest { assertTrue(channels.contains("ch1")) assertTrue(channels.contains("ch2")) } + + @Test + fun `banPlayer refuses a target holding the moderation bypass`() { + val ownerId = createTestUUID(1) + val protectedPlayer = createTestUUID(2) + val (membership, channelManager, _) = createManagers(playersWithBypass = setOf(protectedPlayer)) + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + + val result = membership.banPlayer(protectedPlayer, "mod-ch") + + assertIs<ChannelPlayerBypassBanException>(result.exceptionOrNull()) + assertFalse(channelManager.isPlayerBanned("mod-ch", protectedPlayer).getOrThrow()) + } + + @Test + fun `banPlayer bans a target without the bypass`() { + val ownerId = createTestUUID(1) + val playerId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + + assertTrue(membership.banPlayer(playerId, "mod-ch").isSuccess) + assertTrue(channelManager.isPlayerBanned("mod-ch", playerId).getOrThrow()) + } + + @Test + fun `kickPlayer refuses a target holding the moderation bypass`() { + val ownerId = createTestUUID(1) + val protectedPlayer = createTestUUID(2) + val (membership, channelManager, _) = createManagers(playersWithBypass = setOf(protectedPlayer)) + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + membership.joinChannel(protectedPlayer, "mod-ch") + + val result = membership.kickPlayer(protectedPlayer, "mod-ch") + + assertIs<ChannelPlayerBypassKickException>(result.exceptionOrNull()) + assertTrue(membership.isMember(protectedPlayer, "mod-ch").getOrThrow()) + } + + @Test + fun `kickPlayer removes a target without the bypass`() { + val ownerId = createTestUUID(1) + val playerId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + membership.joinChannel(playerId, "mod-ch") + + assertTrue(membership.kickPlayer(playerId, "mod-ch").isSuccess) + assertFalse(membership.isMember(playerId, "mod-ch").getOrThrow()) + } + + @Test + fun `inviteToChannel refuses an actor inviting themselves`() { + val ownerId = createTestUUID(1) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + + val result = membership.inviteToChannel(ownerId, ownerId, "mod-ch") + + assertIs<ChannelCannotInviteSelfException>(result.exceptionOrNull()) + } + + @Test + fun `inviteToChannel admits a guest to a private channel`() { + val ownerId = createTestUUID(1) + val guestId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId).copy(isPrivate = true)) + + assertTrue(membership.inviteToChannel(ownerId, guestId, "mod-ch").isSuccess) + assertTrue(membership.isMember(guestId, "mod-ch").getOrThrow()) + } + + @Test + fun `inviteToChannel still refuses a banned guest`() { + val ownerId = createTestUUID(1) + val bannedId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + membership.banPlayer(bannedId, "mod-ch") + + val result = membership.inviteToChannel(ownerId, bannedId, "mod-ch") + + assertIs<ChannelPlayerBannedException>(result.exceptionOrNull()) + } } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/ApplyMethodPermissionTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/ApplyMethodPermissionTest.kt deleted file mode 100644 index 1625ebd..0000000 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/ApplyMethodPermissionTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.m1sk9.lunaticChat.paper.command.core - -import com.mojang.brigadier.builder.LiteralArgumentBuilder -import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode -import dev.m1sk9.lunaticChat.paper.LunaticChat -import dev.m1sk9.lunaticChat.paper.command.annotation.Permission -import io.mockk.every -import io.mockk.mockk -import io.papermc.paper.command.brigadier.CommandSourceStack -import io.papermc.paper.command.brigadier.Commands -import kotlin.test.Test -import kotlin.test.assertNotNull -import kotlin.test.assertSame - -@Suppress("UnstableApiUsage") -class ApplyMethodPermissionTest { - /** - * Test helper that exposes [LunaticCommand.applyMethodPermission] and has - * an annotated method for testing. - */ - private class TestableCommand( - plugin: LunaticChat, - ) : LunaticCommand(plugin) { - override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = Commands.literal("test") - - /** Expose the protected applyMethodPermission for testing. */ - fun testApplyMethodPermission( - methodName: String, - builder: LiteralArgumentBuilder<CommandSourceStack>, - ): LiteralArgumentBuilder<CommandSourceStack> = applyMethodPermission(methodName, builder) - - @Permission(LunaticChatPermissionNode.Status::class) - fun annotatedMethod(): LiteralArgumentBuilder<CommandSourceStack> = Commands.literal("annotated") - - fun unannotatedMethod(): LiteralArgumentBuilder<CommandSourceStack> = Commands.literal("unannotated") - } - - private val plugin = mockk<LunaticChat>(relaxed = true) - private val command = TestableCommand(plugin) - - @Test - fun `applyMethodPermission adds requirement when method has Permission annotation`() { - val builder = Commands.literal("test") - val result = command.testApplyMethodPermission("annotatedMethod", builder) - - // The builder should have a requirement set (not the default always-true) - val source = mockk<CommandSourceStack>() - every { source.sender.hasPermission("lunaticchat.command.lc.status") } returns false - assertNotNull(result.requirement) - } - - @Test - fun `applyMethodPermission returns builder unchanged when method has no Permission annotation`() { - val builder = Commands.literal("test") - val result = command.testApplyMethodPermission("unannotatedMethod", builder) - - assertSame(builder, result) - } - - @Test - fun `applyMethodPermission returns builder unchanged when method does not exist`() { - val builder = Commands.literal("test") - val result = command.testApplyMethodPermission("nonExistentMethod", builder) - - assertSame(builder, result) - } -} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommandTest.kt new file mode 100644 index 0000000..16ff92d --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommandTest.kt @@ -0,0 +1,74 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import dev.m1sk9.lunaticChat.paper.LunaticChat +import io.mockk.every +import io.mockk.mockk +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@Suppress("UnstableApiUsage") +class LunaticSubCommandTest { + private class GatedSubCommand( + plugin: LunaticChat, + override val permissionNode: LunaticChatPermissionNode?, + override val aliases: List<String> = emptyList(), + ) : LunaticSubCommand(plugin) { + override val literal = "status" + + override fun build(): LiteralArgumentBuilder<CommandSourceStack> = Commands.literal(literal) + } + + private val plugin = mockk<LunaticChat>(relaxed = true) + + private fun sourceWith( + permission: String, + granted: Boolean, + ): CommandSourceStack = + mockk<CommandSourceStack>().also { + every { it.sender.hasPermission(permission) } returns granted + } + + @Test + fun `buildAll admits a sender holding the declared permission`() { + val command = GatedSubCommand(plugin, LunaticChatPermissionNode.Status) + + val primary = command.buildAll().first() + + assertTrue(primary.requirement.test(sourceWith("lunaticchat.command.lc.status", granted = true))) + } + + @Test + fun `buildAll rejects a sender lacking the declared permission`() { + val command = GatedSubCommand(plugin, LunaticChatPermissionNode.Status) + + val primary = command.buildAll().first() + + assertFalse(primary.requirement.test(sourceWith("lunaticchat.command.lc.status", granted = false))) + } + + @Test + fun `buildAll leaves the node ungated when no permission is declared`() { + val command = GatedSubCommand(plugin, permissionNode = null) + + val primary = command.buildAll().first() + + assertTrue(primary.requirement.test(mockk<CommandSourceStack>())) + } + + @Test + fun `buildAll gates alias nodes the same as the primary`() { + val command = GatedSubCommand(plugin, LunaticChatPermissionNode.Status, aliases = listOf("st")) + + val nodes = command.buildAll() + + assertEquals(2, nodes.size) + assertEquals("st", nodes[1].literal) + assertFalse(nodes[1].requirement.test(sourceWith("lunaticchat.command.lc.status", granted = false))) + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt index 59fc98a..732ee52 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.chat.channel.Channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -72,18 +71,18 @@ class ChannelBanCommandTest { fun `execute should return SuccessWithMessage on ban`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L) - every { channelManager.banPlayer(channelId, targetUUID) } returns Result.success(channel) + every { membershipManager.banPlayer(targetUUID, channelId) } returns Result.success(Unit) every { channelManager.getChannel(channelId) } returns Result.success(channel) val result = command.execute(ctx, "TargetPlayer") assertIs<CommandResult.SuccessWithMessage>(result) - verify { channelManager.banPlayer(channelId, targetUUID) } + verify { membershipManager.banPlayer(targetUUID, channelId) } } @Test @@ -100,7 +99,7 @@ class ChannelBanCommandTest { fun `execute should return Failure when no permission`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -111,7 +110,7 @@ class ChannelBanCommandTest { fun `execute should return Failure when player not found`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer(hasPlayedBefore = false, isOnline = false) val result = command.execute(ctx, "TargetPlayer") @@ -123,11 +122,11 @@ class ChannelBanCommandTest { fun `execute should return Failure when already banned`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null - every { channelManager.banPlayer(channelId, targetUUID) } returns + every { membershipManager.banPlayer(targetUUID, channelId) } returns Result.failure(ChannelPlayerAlreadyBannedException(targetUUID, channelId)) val result = command.execute(ctx, "TargetPlayer") diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt index c51f460..3682c74 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt @@ -1,7 +1,8 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.TestUtils import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -53,9 +54,8 @@ class ChannelInviteCommandTest { val channel = TestUtils.createTestChannel(id = channelId, ownerId = testUUID) every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER - every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(false) - every { deps.membershipManager.joinChannel(targetUUID, channelId, bypassPrivateCheck = true) } returns Result.success(Unit) + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) + every { deps.membershipManager.inviteToChannel(testUUID, targetUUID, channelId) } returns Result.success(Unit) every { deps.channelManager.getChannel(channelId) } returns Result.success(channel) mockkStatic(Bukkit::class) @@ -76,11 +76,13 @@ class ChannelInviteCommandTest { val selfTarget = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1") every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) mockkStatic(Bukkit::class) try { every { Bukkit.getPlayer(any<String>()) } returns selfTarget + every { deps.membershipManager.inviteToChannel(testUUID, testUUID, channelId) } returns + Result.failure(ChannelCannotInviteSelfException(testUUID)) val result = deps.command.execute(deps.ctx, "Player1") @@ -96,8 +98,9 @@ class ChannelInviteCommandTest { val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer") every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER - every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(true) + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) + every { deps.membershipManager.inviteToChannel(testUUID, targetUUID, channelId) } returns + Result.failure(ChannelPlayerBannedException(targetUUID, channelId)) mockkStatic(Bukkit::class) try { @@ -116,7 +119,7 @@ class ChannelInviteCommandTest { val deps = createDependencies() every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) mockkStatic(Bukkit::class) try { @@ -136,7 +139,7 @@ class ChannelInviteCommandTest { val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer") every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) mockkStatic(Bukkit::class) try { diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt index cab6ef0..23032c1 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.chat.channel.Channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.TestUtils @@ -71,20 +70,20 @@ class ChannelKickCommandTest { fun `execute should return SuccessWithMessage on kick`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null every { membershipManager.isMember(targetUUID, channelId) } returns Result.success(true) val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L) - every { channelManager.removeMember(channelId, targetUUID) } returns Result.success(Unit) + every { membershipManager.kickPlayer(targetUUID, channelId) } returns Result.success(Unit) every { channelManager.getPlayerChannel(targetUUID) } returns channelId every { channelManager.getChannel(channelId) } returns Result.success(channel) val result = command.execute(ctx, "TargetPlayer") assertIs<CommandResult.SuccessWithMessage>(result) - verify { channelManager.removeMember(channelId, targetUUID) } + verify { membershipManager.kickPlayer(targetUUID, channelId) } } @Test @@ -101,7 +100,7 @@ class ChannelKickCommandTest { fun `execute should return Failure when no permission`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -112,7 +111,7 @@ class ChannelKickCommandTest { fun `execute should return Failure when target not member`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null every { membershipManager.isMember(targetUUID, channelId) } returns Result.success(false) @@ -126,7 +125,7 @@ class ChannelKickCommandTest { fun `execute should return Failure when player not found`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer(hasPlayedBefore = false, isOnline = false) val result = command.execute(ctx, "TargetPlayer") diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt index da7c7e2..bd9802c 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt @@ -69,7 +69,7 @@ class ChannelModCommandTest { fun `execute should promote to moderator`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MEMBER @@ -88,7 +88,7 @@ class ChannelModCommandTest { fun `execute should demote from moderator`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MODERATOR @@ -107,7 +107,7 @@ class ChannelModCommandTest { fun `execute should return Failure when modding self`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) val offlinePlayer = mockk<OfflinePlayer>(relaxed = true) every { offlinePlayer.uniqueId } returns testUUID @@ -125,7 +125,7 @@ class ChannelModCommandTest { fun `execute should return Failure when not owner`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MODERATOR + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -136,7 +136,7 @@ class ChannelModCommandTest { fun `execute should return Failure when target not member`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns null diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt index 9f25264..8f0d7d1 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt @@ -69,7 +69,7 @@ class ChannelOwnershipCommandTest { fun `execute should transfer ownership successfully`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MEMBER @@ -87,7 +87,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when transferring to self`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) val offlinePlayer = mockk<OfflinePlayer>(relaxed = true) every { offlinePlayer.uniqueId } returns testUUID @@ -105,7 +105,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when target not member`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns null @@ -118,7 +118,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when not owner`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MODERATOR + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -129,7 +129,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when player not found`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer(hasPlayedBefore = false, isOnline = false) val result = command.execute(ctx, "TargetPlayer") diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt index 16dd383..93d5749 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.chat.channel.Channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -70,7 +69,7 @@ class ChannelUnbanCommandTest { fun `execute should return SuccessWithMessage on unban`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L) @@ -97,7 +96,7 @@ class ChannelUnbanCommandTest { fun `execute should return Failure when no permission`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -108,7 +107,7 @@ class ChannelUnbanCommandTest { fun `execute should return Failure when player not banned`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { channelManager.unbanPlayer(channelId, targetUUID) } returns diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt index 52927a8..e05426e 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerRegistryTest.kt @@ -1,7 +1,8 @@ package dev.m1sk9.lunaticChat.paper.command.setting -import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager +import io.mockk.mockk import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -9,16 +10,7 @@ import kotlin.test.assertTrue class SettingHandlerRegistryTest { private fun createMockHandler(settingKey: SettingKey): SettingHandler = - object : SettingHandler { - override val key: SettingKey = settingKey - - override fun execute( - ctx: CommandContext, - enable: Boolean, - ): CommandResult = CommandResult.Success - - override fun showStatus(ctx: CommandContext): CommandResult = CommandResult.Success - } + SettingHandler(settingKey, mockk<PlayerSettingsManager>(relaxed = true), mockk<LanguageManager>(relaxed = true)) @Test fun `register should make handler retrievable`() { diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerTest.kt new file mode 100644 index 0000000..3e80ba0 --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerTest.kt @@ -0,0 +1,106 @@ +package dev.m1sk9.lunaticChat.paper.command.setting + +import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.paper.TestUtils +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import net.kyori.adventure.text.Component +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class SettingHandlerTest { + private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000") + + private class Fixture( + key: SettingKey, + uuid: UUID, + ) { + val ctx = mockk<CommandContext>(relaxed = true) + val settingsManager = mockk<PlayerSettingsManager>(relaxed = true) + val languageManager = mockk<LanguageManager>(relaxed = true) + val handler: SettingHandler + + init { + every { ctx.requirePlayer() } returns TestUtils.createMockPlayer(uuid = uuid, name = "TestPlayer") + every { settingsManager.getSettings(uuid) } returns TestUtils.createTestPlayerSettings(uuid = uuid) + every { languageManager.getMessage(any(), any()) } returns "test message" + every { languageManager.getToggleText(any()) } returns "ON" + handler = SettingHandler(key, settingsManager, languageManager) + } + } + + private fun eachKey(assertion: (SettingKey, Fixture) -> Unit) = + SettingKey.values().forEach { key -> assertion(key, Fixture(key, testUUID)) } + + @Test + fun `execute enable writes the setting as enabled`() = + eachKey { key, f -> + assertIs<CommandResult.Success>(f.handler.execute(f.ctx, true)) + verify { f.settingsManager.updateSettings(match { key.read(it) }) } + } + + @Test + fun `execute disable writes the setting as disabled`() = + eachKey { key, f -> + assertIs<CommandResult.Success>(f.handler.execute(f.ctx, false)) + verify { f.settingsManager.updateSettings(match { !key.read(it) }) } + } + + @Test + fun `execute reports the change with the toggle message`() = + eachKey { key, f -> + f.handler.execute(f.ctx, true) + + val player = f.ctx.requirePlayer() + verify { f.languageManager.getMessage(key.toggleMessageKey, mapOf("toggle" to "ON")) } + verify { player.sendMessage(any<Component>()) } + } + + @Test + fun `showStatus reports the current value with the status message`() = + eachKey { key, f -> + assertIs<CommandResult.Success>(f.handler.showStatus(f.ctx)) + + val player = f.ctx.requirePlayer() + verify { f.languageManager.getMessage(key.statusMessageKey, mapOf("toggle" to "ON")) } + verify { player.sendMessage(any<Component>()) } + } + + @Test + fun `handler exposes the key it was built for`() = eachKey { key, f -> assertEquals(key, f.handler.key) } + + @Test + fun `each key round-trips through write and read`() { + val settings = TestUtils.createTestPlayerSettings(uuid = testUUID) + + SettingKey.values().forEach { key -> + assertTrue(key.read(key.write(settings, true)), key.key) + assertFalse(key.read(key.write(settings, false)), key.key) + } + } + + @Test + fun `writing one key leaves the other settings untouched`() { + val original = TestUtils.createTestPlayerSettings(uuid = testUUID) + + SettingKey.values().forEach { key -> + val flipped = key.write(original, !key.read(original)) + + SettingKey.values().filterNot { it == key }.forEach { other -> + assertEquals( + other.read(original), + other.read(flipped), + "${other.key} changed while writing ${key.key}", + ) + } + } + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/SettingHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/SettingHandlerTest.kt deleted file mode 100644 index 1967e1c..0000000 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/SettingHandlerTest.kt +++ /dev/null @@ -1,191 +0,0 @@ -package dev.m1sk9.lunaticChat.paper.command.setting.handler - -import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.paper.TestUtils -import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey -import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager -import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import net.kyori.adventure.text.Component -import java.util.UUID -import kotlin.test.Test -import kotlin.test.assertIs - -class SettingHandlerTest { - private val testUUID = UUID.fromString("00000001-0000-0000-0000-000000000000") - - private fun createDependencies(): Triple<CommandContext, PlayerSettingsManager, LanguageManager> { - val player = TestUtils.createMockPlayer(uuid = testUUID, name = "TestPlayer") - val ctx = mockk<CommandContext>(relaxed = true) - every { ctx.requirePlayer() } returns player - - val settingsManager = mockk<PlayerSettingsManager>(relaxed = true) - every { settingsManager.getSettings(testUUID) } returns - TestUtils.createTestPlayerSettings(uuid = testUUID) - - val languageManager = mockk<LanguageManager>(relaxed = true) - every { languageManager.getMessage(any(), any()) } returns "test message" - every { languageManager.getToggleText(any()) } returns "ON" - - return Triple(ctx, settingsManager, languageManager) - } - - // --- JapaneseConversionSettingHandler --- - - @Test - fun `JapaneseConversionSettingHandler key should be Japanese`() { - val (_, settingsManager, languageManager) = createDependencies() - val handler = JapaneseConversionSettingHandler(settingsManager, languageManager) - assertIs<SettingKey.Japanese>(handler.key) - } - - @Test - fun `JapaneseConversionSettingHandler execute enable should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = JapaneseConversionSettingHandler(settingsManager, languageManager) - - val result = handler.execute(ctx, true) - - assertIs<CommandResult.Success>(result) - verify { settingsManager.updateSettings(match { it.japaneseConversionEnabled }) } - } - - @Test - fun `JapaneseConversionSettingHandler execute disable should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = JapaneseConversionSettingHandler(settingsManager, languageManager) - - val result = handler.execute(ctx, false) - - assertIs<CommandResult.Success>(result) - verify { settingsManager.updateSettings(match { !it.japaneseConversionEnabled }) } - } - - @Test - fun `JapaneseConversionSettingHandler showStatus should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = JapaneseConversionSettingHandler(settingsManager, languageManager) - - val result = handler.showStatus(ctx) - - assertIs<CommandResult.Success>(result) - } - - @Test - fun `JapaneseConversionSettingHandler execute should send message to player`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = JapaneseConversionSettingHandler(settingsManager, languageManager) - - handler.execute(ctx, true) - - val player = ctx.requirePlayer() - verify { player.sendMessage(any<Component>()) } - } - - // --- DirectMessageNoticeSettingHandler --- - - @Test - fun `DirectMessageNoticeSettingHandler key should be Notice`() { - val (_, settingsManager, languageManager) = createDependencies() - val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager) - assertIs<SettingKey.Notice>(handler.key) - } - - @Test - fun `DirectMessageNoticeSettingHandler execute enable should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager) - - val result = handler.execute(ctx, true) - - assertIs<CommandResult.Success>(result) - verify { settingsManager.updateSettings(match { it.directMessageNotificationEnabled }) } - } - - @Test - fun `DirectMessageNoticeSettingHandler execute disable should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager) - - val result = handler.execute(ctx, false) - - assertIs<CommandResult.Success>(result) - verify { settingsManager.updateSettings(match { !it.directMessageNotificationEnabled }) } - } - - @Test - fun `DirectMessageNoticeSettingHandler showStatus should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager) - - val result = handler.showStatus(ctx) - - assertIs<CommandResult.Success>(result) - } - - @Test - fun `DirectMessageNoticeSettingHandler execute should send message to player`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager) - - handler.execute(ctx, true) - - val player = ctx.requirePlayer() - verify { player.sendMessage(any<Component>()) } - } - - // --- ChannelMessageNoticeSettingHandler --- - - @Test - fun `ChannelMessageNoticeSettingHandler key should be ChNotice`() { - val (_, settingsManager, languageManager) = createDependencies() - val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager) - assertIs<SettingKey.ChNotice>(handler.key) - } - - @Test - fun `ChannelMessageNoticeSettingHandler execute enable should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager) - - val result = handler.execute(ctx, true) - - assertIs<CommandResult.Success>(result) - verify { settingsManager.updateSettings(match { it.channelMessageNotificationEnabled }) } - } - - @Test - fun `ChannelMessageNoticeSettingHandler execute disable should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager) - - val result = handler.execute(ctx, false) - - assertIs<CommandResult.Success>(result) - verify { settingsManager.updateSettings(match { !it.channelMessageNotificationEnabled }) } - } - - @Test - fun `ChannelMessageNoticeSettingHandler showStatus should return Success`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager) - - val result = handler.showStatus(ctx) - - assertIs<CommandResult.Success>(result) - } - - @Test - fun `ChannelMessageNoticeSettingHandler execute should send message to player`() { - val (ctx, settingsManager, languageManager) = createDependencies() - val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager) - - handler.execute(ctx, true) - - val player = ctx.requirePlayer() - verify { player.sendMessage(any<Component>()) } - } -} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt index bc716c9..b7e1385 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt @@ -6,6 +6,7 @@ import dev.m1sk9.lunaticChat.paper.TestUtils import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID import io.mockk.every import io.mockk.mockk +import io.mockk.slot import io.mockk.verify import kotlin.test.Test import kotlin.test.assertEquals @@ -82,6 +83,28 @@ class PlayerSettingsManagerTest { } @Test + fun `a queued save writes changes made after it was queued`() { + val (manager, storage, _) = createManager() + manager.initialize() + + // Only the first request of a debounced burst survives; the write it schedules must still + // see every later change, or those changes exist only in memory until the next write. + val scheduled = slot<() -> PlayerSettingsData>() + every { storage.queueAsyncSave(capture(scheduled)) } returns Unit + + val first = createTestUUID(1) + val second = createTestUUID(2) + manager.updateSettings(PlayerChatSettings(uuid = first, japaneseConversionEnabled = false)) + val pendingWrite = scheduled.captured + manager.updateSettings(PlayerChatSettings(uuid = second, japaneseConversionEnabled = false)) + + val written = pendingWrite() + + assertEquals(false, written.japaneseConversion[first]) + assertEquals(false, written.japaneseConversion[second]) + } + + @Test fun `updateSettings should overwrite existing settings`() { val playerId = createTestUUID(1) val data = diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCacheTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCacheTest.kt new file mode 100644 index 0000000..316b455 --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCacheTest.kt @@ -0,0 +1,44 @@ +package dev.m1sk9.lunaticChat.paper.velocity + +import io.mockk.mockk +import java.util.logging.Logger +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MessageDeduplicationCacheTest { + private fun cache(cacheSize: Int) = MessageDeduplicationCache(cacheSize, mockk<Logger>(relaxed = true), "test") + + @Test + fun `an unseen message id is new`() { + assertTrue(cache(10).isNew("m1")) + } + + @Test + fun `a recorded message id is no longer new`() { + val cache = cache(10) + + cache.markProcessed("m1") + + assertFalse(cache.isNew("m1")) + } + + @Test + fun `recording one id does not mask another`() { + val cache = cache(10) + + cache.markProcessed("m1") + + assertTrue(cache.isNew("m2")) + } + + @Test + fun `eviction keeps the cache from growing without bound`() { + val cache = cache(4) + + repeat(100) { cache.markProcessed("m$it") } + + val remembered = (0 until 100).count { !cache.isNew("m$it") } + assertTrue(remembered <= 4, "expected at most 4 retained entries, got $remembered") + } +} diff --git a/platform-velocity/build.gradle.kts b/platform-velocity/build.gradle.kts index 3cd8c2d..65bd27f 100644 --- a/platform-velocity/build.gradle.kts +++ b/platform-velocity/build.gradle.kts @@ -57,9 +57,6 @@ tasks { filesMatching("velocity-plugin.json") { expand(props) } - filesMatching("build-info.properties") { - expand(props) - } } build { diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/BuildInfo.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/BuildInfo.kt deleted file mode 100644 index 1cb13d4..0000000 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/BuildInfo.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.m1sk9.lunaticChat.velocity - -import java.util.Properties - -object BuildInfo { - val version: String - val commitHash: String - val channel: String - - init { - val props = Properties() - BuildInfo::class.java.getResourceAsStream("/build-info.properties")?.use { - props.load(it) - } - version = props.getProperty("version", "unknown") - commitHash = props.getProperty("commit", "unknown") - channel = props.getProperty("channel", "stable") - } - - val isNightly: Boolean get() = channel == "nightly" - - fun versionWithCommit(): String = "$version ($commitHash)" -} diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelay.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelay.kt index bedc302..f47db70 100644 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelay.kt +++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerChatRelay.kt @@ -4,6 +4,7 @@ import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier import com.velocitypowered.api.proxy.server.RegisteredServer import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import org.slf4j.Logger @@ -18,7 +19,7 @@ class CrossServerChatRelay( private val logger: Logger, ) { companion object { - private val CHANNEL = MinecraftChannelIdentifier.create("lunaticchat", "main") + private val CHANNEL = MinecraftChannelIdentifier.create(PluginMessageChannel.NAMESPACE, PluginMessageChannel.NAME) } /** diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt index 89cc15f..3746395 100644 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt +++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt @@ -4,6 +4,7 @@ import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier import com.velocitypowered.api.proxy.server.RegisteredServer import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import org.slf4j.Logger @@ -19,7 +20,7 @@ class CrossServerDirectMessageRelay( private val logger: Logger, ) { companion object { - private val CHANNEL = MinecraftChannelIdentifier.create("lunaticchat", "main") + private val CHANNEL = MinecraftChannelIdentifier.create(PluginMessageChannel.NAMESPACE, PluginMessageChannel.NAME) } /** diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt index 4472d1c..6b40a7e 100644 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt +++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt @@ -6,6 +6,7 @@ import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.proxy.ServerConnection import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import dev.m1sk9.lunaticChat.engine.protocol.ProtocolVersion import dev.m1sk9.lunaticChat.velocity.presence.PresenceTracker @@ -30,7 +31,7 @@ class PluginMessageHandler( private val presenceTracker: PresenceTracker, ) { companion object { - private val CHANNEL = MinecraftChannelIdentifier.create("lunaticchat", "main") + private val CHANNEL = MinecraftChannelIdentifier.create(PluginMessageChannel.NAMESPACE, PluginMessageChannel.NAME) } /** diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/presence/PresenceTracker.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/presence/PresenceTracker.kt index f9442ee..59dbb5b 100644 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/presence/PresenceTracker.kt +++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/presence/PresenceTracker.kt @@ -8,6 +8,7 @@ import com.velocitypowered.api.proxy.ProxyServer import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier import com.velocitypowered.api.proxy.server.RegisteredServer import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage +import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageChannel import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec import dev.m1sk9.lunaticChat.engine.protocol.PresenceEntry import org.slf4j.Logger @@ -28,7 +29,7 @@ class PresenceTracker( private val logger: Logger, ) { companion object { - private val CHANNEL = MinecraftChannelIdentifier.create("lunaticchat", "main") + private val CHANNEL = MinecraftChannelIdentifier.create(PluginMessageChannel.NAMESPACE, PluginMessageChannel.NAME) } /** diff --git a/platform-velocity/src/main/resources/build-info.properties b/platform-velocity/src/main/resources/build-info.properties deleted file mode 100644 index 1f8dbc2..0000000 --- a/platform-velocity/src/main/resources/build-info.properties +++ /dev/null @@ -1,3 +0,0 @@ -version=${version} -commit=${gitCommitHash} -channel=${channel} |
