From bc0d7d522e169f3eeb8d5b632ee673392060694b Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:10:46 +0900 Subject: refactor: remove code that no production path reaches These were all scaffolding that drifted out of use, and each one costs a reader time before they discover it does nothing: - UUIDASStringSerializer duplicated UUIDSerializer byte for byte; the differing descriptor name never reaches the JSON/YAML wire format, so the choice between them was a coin flip for contributors. - Velocity's BuildInfo was never referenced (the plugin reads its version from PluginContainer) and read a "commit" property the build never wrote, so it would have reported "unknown" had anyone called it. - KanaConverter.TrieNode.Leaf is never constructed: buildTrie starts from a Branch and insert only ever returns Branch. Six branches guarded against a state the type system allowed but the code could not produce. With those gone, isValidRomaji and toHiragana were visibly the same trie walk, so they now share one longestMatch. - @Deprecated command handling had no annotated command to act on. - The settings backup restore looked for *.backup.* files that nothing in the repository writes, so it always fell through to empty settings. Also drops CommandContext.replyWithEvent/replyPlain, PluginCoroutineScope's unused plugin parameter, GitHubRelease fields no caller reads, and four language keys with no lookup site. Co-Authored-By: Claude --- .../lunaticChat/engine/converter/KanaConverter.kt | 123 ++++++--------------- .../engine/settings/PlayerSettingsData.kt | 6 +- .../engine/settings/UUIDASStringSerializer.kt | 27 ----- .../lunaticChat/engine/settings/UUIDSerializer.kt | 2 + .../engine/settings/UUIDSerializerTest.kt | 44 -------- .../dev/m1sk9/lunaticChat/paper/LunaticChat.kt | 2 +- .../lunaticChat/paper/PluginCoroutineScope.kt | 2 - .../paper/command/core/CommandContext.kt | 25 ----- .../paper/command/core/LunaticCommand.kt | 18 --- .../lunaticChat/paper/common/UpdateChecker.kt | 6 - .../paper/settings/YamlPlayerSettingsStorage.kt | 33 +----- platform-paper/src/main/resources/languages/en.yml | 4 - platform-paper/src/main/resources/languages/ja.yml | 4 - platform-velocity/build.gradle.kts | 3 - .../dev/m1sk9/lunaticChat/velocity/BuildInfo.kt | 23 ---- .../src/main/resources/build-info.properties | 3 - 16 files changed, 43 insertions(+), 282 deletions(-) delete mode 100644 engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt delete mode 100644 platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/BuildInfo.kt delete mode 100644 platform-velocity/src/main/resources/build-info.properties 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, - val value: String? = null, - ) : TrieNode() - } + private class TrieNode( + val children: Map, + 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? { + var node = romanjiTrie + var match: Pair? = 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? = 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/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 { - 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 { override val descriptor: SerialDescriptor = 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") @@ -51,35 +45,6 @@ class UUIDSerializerTest { assertEquals(originalUuid, decoded.uuid) } - @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 { - json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) - } - } } 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..e006299 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 @@ -60,7 +60,7 @@ class LunaticChat : val httpClient = HttpClient(CIO) // Initialize plugin coroutine scope - pluginScope = PluginCoroutineScope(this, logger) + pluginScope = PluginCoroutineScope(logger) // Initialize all services serviceInitializer = 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/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..2447ec4 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 @@ -32,10 +32,6 @@ abstract class LunaticCommand( this::class.annotations.filterIsInstance().firstOrNull() } - private val deprecatedAnnotation: Deprecated? by lazy { - this::class.annotations.filterIsInstance().firstOrNull() - } - private val isPlayerOnly: Boolean by lazy { this::class.annotations.any { it is PlayerOnly } } @@ -65,20 +61,6 @@ abstract class LunaticCommand( * Called by CommandRegistry during registration. */ fun buildWithChecks(): LiteralArgumentBuilder { - // 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 = 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/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt index 5a2c8e2..5513303 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 @@ -9,12 +9,11 @@ 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 @@ -30,8 +29,7 @@ class YamlPlayerSettingsStorage( /** * 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 +44,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() } @@ -98,19 +84,4 @@ class YamlPlayerSettingsStorage( ) } } - - /** - * 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/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-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/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} -- cgit v1.2.1 From 1510404f1f8cf31696fb4ca7457955842267b631 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:17:16 +0900 Subject: refactor: give subcommands their own base type Subcommands were LunaticCommands that could not be commands. Each of the 17 carried the same three-method preamble plus an override of buildCommand() that only threw, because the contract it inherited - @Command-driven name, aliases and description - has no meaning for a node attached under a parent literal. Accessing name on any of them would have thrown too. LunaticSubCommand now models that node directly, so the preamble and the throwing override are gone. Two knock-on wins: - The permission gate is a declared property instead of applyMethodPermission("build", ...), which looked the method up by name through reflection and returned the builder ungated whenever the lookup or the annotation was missing. A rename or a dropped annotation silently opened the command up; now omitting the gate fails to compile. - Subcommands expose the literal they register under, so ChannelCommand drives both its Brigadier tree and its help output from one list. The two 14-entry lists it maintained by hand could disagree without any compiler or test noticing. ApplyMethodPermissionTest covered the reflection helper that no longer exists; LunaticSubCommandTest covers the gate that replaced it. Co-Authored-By: Claude --- .../paper/command/core/LunaticCommand.kt | 112 +------- .../paper/command/core/LunaticCommandBase.kt | 92 +++++++ .../paper/command/core/LunaticSubCommand.kt | 44 ++++ .../paper/command/impl/lc/ChannelCommand.kt | 285 ++++----------------- .../paper/command/impl/lc/LunaticChatCommand.kt | 6 +- .../paper/command/impl/lc/SettingsCommand.kt | 29 +-- .../paper/command/impl/lc/StatusCommand.kt | 25 +- .../command/impl/lc/channel/ChannelBanCommand.kt | 24 +- .../impl/lc/channel/ChannelCreateCommand.kt | 25 +- .../impl/lc/channel/ChannelDeleteCommand.kt | 25 +- .../command/impl/lc/channel/ChannelInfoCommand.kt | 25 +- .../impl/lc/channel/ChannelInviteCommand.kt | 25 +- .../command/impl/lc/channel/ChannelJoinCommand.kt | 25 +- .../command/impl/lc/channel/ChannelKickCommand.kt | 25 +- .../command/impl/lc/channel/ChannelLeaveCommand.kt | 25 +- .../command/impl/lc/channel/ChannelListCommand.kt | 25 +- .../command/impl/lc/channel/ChannelModCommand.kt | 24 +- .../impl/lc/channel/ChannelOwnershipCommand.kt | 25 +- .../impl/lc/channel/ChannelStatusCommand.kt | 25 +- .../impl/lc/channel/ChannelSwitchCommand.kt | 25 +- .../command/impl/lc/channel/ChannelUnbanCommand.kt | 24 +- .../command/core/ApplyMethodPermissionTest.kt | 67 ----- .../paper/command/core/LunaticSubCommandTest.kt | 74 ++++++ 23 files changed, 372 insertions(+), 709 deletions(-) create mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommandBase.kt create mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommand.kt delete mode 100644 platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/ApplyMethodPermissionTest.kt create mode 100644 platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticSubCommandTest.kt 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 2447ec4..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().firstOrNull() ?: throw IllegalStateException("Command class must be annotated with @Command") @@ -32,10 +24,6 @@ abstract class LunaticCommand( this::class.annotations.filterIsInstance().firstOrNull() } - private val isPlayerOnly: Boolean by lazy { - this::class.annotations.any { it is PlayerOnly } - } - /** The primary command name */ val name: String get() = commandAnnotation.name @@ -71,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): 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, - aliases: List, - ): List> { - 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, - ): LiteralArgumentBuilder { - val method = this::class.memberFunctions.find { it.name == methodName } ?: return builder - val permissionAnnotation = method.findAnnotation() ?: 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..596d70d --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommandBase.kt @@ -0,0 +1,92 @@ +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.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 } + } + + /** + * 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): 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, + aliases: List, + ): List> { + 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 = emptyList() + + /** + * Builds the primary subcommand node, without the permission gate. + */ + abstract fun build(): LiteralArgumentBuilder + + /** + * Builds the primary node and its aliases, each gated on [permissionNode]. + */ + fun buildAll(): List> { + 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/lc/ChannelCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt index 13c30dd..b3a1981 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 @@ -38,117 +37,17 @@ class ChannelCommand( private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("ch")) - - @Permission(LunaticChatPermissionNode.Channel::class) - fun build(): LiteralArgumentBuilder { - 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) } +) : 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 { + 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 = + 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 = - 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..bd9ca9b 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 @@ -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..e8f66d4 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 @@ -29,18 +28,10 @@ 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 { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("set")) +) : 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 off * - /lc setting (shows status) */ - @Permission(LunaticChatPermissionNode.Settings::class) - fun build(): LiteralArgumentBuilder { - val settingCommand = Commands.literal("settings") + override fun build(): LiteralArgumentBuilder { + 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 = - 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..f47285e 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 @@ -23,18 +22,13 @@ class StatusCommand( plugin: LunaticChat, private val languageManager: LanguageManager, private val configuration: LunaticChatConfiguration, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - 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 = - Commands.literal("status").executes { ctx -> + override fun build(): LiteralArgumentBuilder = + Commands.literal(literal).executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -177,9 +171,4 @@ class StatusCommand( .append(Component.text("$label: ", NamedTextColor.GRAY)) .append(Component.text(toggleText, color)) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..c459ca0 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 @@ -12,10 +12,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.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -29,19 +28,13 @@ class ChannelBanCommand( private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), emptyList()) +) : LunaticSubCommand(plugin) { + override val literal = "ban" + override val permissionNode = LunaticChatPermissionNode.ChannelBan - @Permission(LunaticChatPermissionNode.ChannelBan::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("ban") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -205,9 +198,4 @@ class ChannelBanCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..83382c6 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 @@ -23,19 +22,14 @@ class ChannelCreateCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("new")) +) : LunaticSubCommand(plugin) { + override val literal = "create" + override val permissionNode = LunaticChatPermissionNode.ChannelCreate + override val aliases = listOf("new") - @Permission(LunaticChatPermissionNode.ChannelCreate::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("create") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -158,9 +152,4 @@ class ChannelCreateCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..6ac6c00 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,10 +8,9 @@ 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 @@ -22,19 +21,14 @@ class ChannelDeleteCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("del")) +) : LunaticSubCommand(plugin) { + override val literal = "delete" + override val permissionNode = LunaticChatPermissionNode.ChannelDelete + override val aliases = listOf("del") - @Permission(LunaticChatPermissionNode.ChannelDelete::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("delete") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -124,9 +118,4 @@ class ChannelDeleteCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..a9b464b 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 @@ -23,23 +22,18 @@ class ChannelInfoCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { +) : LunaticSubCommand(plugin) { companion object { private const val MAX_MEMBERS_DISPLAY = 10 } - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("i")) + override val literal = "info" + override val permissionNode = LunaticChatPermissionNode.ChannelInfo + override val aliases = listOf("i") - @Permission(LunaticChatPermissionNode.ChannelInfo::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("info") + .literal(literal) .executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -174,9 +168,4 @@ class ChannelInfoCommand( return CommandResult.Success } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..096d80c 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 @@ -11,10 +11,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,19 +26,14 @@ class ChannelInviteCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("inv")) +) : LunaticSubCommand(plugin) { + override val literal = "invite" + override val permissionNode = LunaticChatPermissionNode.ChannelInvite + override val aliases = listOf("inv") - @Permission(LunaticChatPermissionNode.ChannelInvite::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("invite") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -187,9 +181,4 @@ class ChannelInviteCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..119e89a 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,10 +15,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.common.playChannelJoinNotification import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter @@ -32,19 +31,14 @@ class ChannelJoinCommand( private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("j")) +) : LunaticSubCommand(plugin) { + override val literal = "join" + override val permissionNode = LunaticChatPermissionNode.ChannelJoin + override val aliases = listOf("j") - @Permission(LunaticChatPermissionNode.ChannelJoin::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("join") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -170,9 +164,4 @@ class ChannelJoinCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..9443e5f 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 @@ -10,10 +10,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.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -27,19 +26,14 @@ class ChannelKickCommand( private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("k")) +) : LunaticSubCommand(plugin) { + override val literal = "kick" + override val permissionNode = LunaticChatPermissionNode.ChannelKick + override val aliases = listOf("k") - @Permission(LunaticChatPermissionNode.ChannelKick::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("kick") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -202,9 +196,4 @@ class ChannelKickCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..e329ca3 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,10 +8,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.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -24,18 +23,13 @@ class ChannelLeaveCommand( private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("l")) +) : LunaticSubCommand(plugin) { + override val literal = "leave" + override val permissionNode = LunaticChatPermissionNode.ChannelLeave + override val aliases = listOf("l") - @Permission(LunaticChatPermissionNode.ChannelLeave::class) - fun build(): LiteralArgumentBuilder = - Commands.literal("leave").executes { ctx -> + override fun build(): LiteralArgumentBuilder = + Commands.literal(literal).executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -87,9 +81,4 @@ class ChannelLeaveCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..949f6cc 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 @@ -25,23 +24,18 @@ class ChannelListCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { +) : LunaticSubCommand(plugin) { companion object { private const val CHANNELS_PER_PAGE = 10 } - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - 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 = + override fun build(): LiteralArgumentBuilder = Commands - .literal("list") + .literal(literal) .executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -197,9 +191,4 @@ class ChannelListCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..8d9be52 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 @@ -9,10 +9,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 @@ -25,19 +24,13 @@ class ChannelModCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), emptyList()) +) : LunaticSubCommand(plugin) { + override val literal = "mod" + override val permissionNode = LunaticChatPermissionNode.ChannelMod - @Permission(LunaticChatPermissionNode.ChannelMod::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("mod") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -193,9 +186,4 @@ class ChannelModCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..20928bf 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 @@ -9,10 +9,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 @@ -25,19 +24,14 @@ class ChannelOwnershipCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("own")) +) : LunaticSubCommand(plugin) { + override val literal = "ownership" + override val permissionNode = LunaticChatPermissionNode.ChannelOwnership + override val aliases = listOf("own") - @Permission(LunaticChatPermissionNode.ChannelOwnership::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("ownership") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -179,9 +173,4 @@ class ChannelOwnershipCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..2da2bb8 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 @@ -28,22 +27,17 @@ class ChannelStatusCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { +) : LunaticSubCommand(plugin) { companion object { private const val MAX_MEMBERS_DISPLAY = 10 } - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("st")) + override val literal = "status" + override val permissionNode = LunaticChatPermissionNode.ChannelStatus + override val aliases = listOf("st") - @Permission(LunaticChatPermissionNode.ChannelStatus::class) - fun build(): LiteralArgumentBuilder = - Commands.literal("status").executes { ctx -> + override fun build(): LiteralArgumentBuilder = + Commands.literal(literal).executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -247,9 +241,4 @@ class ChannelStatusCommand( return CommandResult.Success } - - override fun buildCommand(): LiteralArgumentBuilder = - 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/ChannelSwitchCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt index b5bd18d..974fd38 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,10 +10,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 @@ -25,19 +24,14 @@ class ChannelSwitchCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), listOf("sw")) +) : LunaticSubCommand(plugin) { + override val literal = "switch" + override val permissionNode = LunaticChatPermissionNode.ChannelSwitch + override val aliases = listOf("sw") - @Permission(LunaticChatPermissionNode.ChannelSwitch::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("switch") + .literal(literal) .then( Commands .argument("channelId", StringArgumentType.word()) @@ -125,9 +119,4 @@ class ChannelSwitchCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - 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..1d89b06 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 @@ -10,10 +10,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 @@ -26,19 +25,13 @@ class ChannelUnbanCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, -) : LunaticCommand(plugin) { - fun buildWithPermissionCheck(): LiteralArgumentBuilder { - val builder = build() - return applyMethodPermission("build", builder) - } - - fun buildAllWithPermissionCheck(): List> = - withAliases(buildWithPermissionCheck(), emptyList()) +) : LunaticSubCommand(plugin) { + override val literal = "unban" + override val permissionNode = LunaticChatPermissionNode.ChannelUnban - @Permission(LunaticChatPermissionNode.ChannelUnban::class) - fun build(): LiteralArgumentBuilder = + override fun build(): LiteralArgumentBuilder = Commands - .literal("unban") + .literal(literal) .then( Commands .argument("playerName", StringArgumentType.word()) @@ -154,9 +147,4 @@ class ChannelUnbanCommand( }, ) } - - override fun buildCommand(): LiteralArgumentBuilder = - throw UnsupportedOperationException( - "ChannelUnbanCommand should use build() method instead of buildCommand()", - ) } 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 = Commands.literal("test") - - /** Expose the protected applyMethodPermission for testing. */ - fun testApplyMethodPermission( - methodName: String, - builder: LiteralArgumentBuilder, - ): LiteralArgumentBuilder = applyMethodPermission(methodName, builder) - - @Permission(LunaticChatPermissionNode.Status::class) - fun annotatedMethod(): LiteralArgumentBuilder = Commands.literal("annotated") - - fun unannotatedMethod(): LiteralArgumentBuilder = Commands.literal("unannotated") - } - - private val plugin = mockk(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() - 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 = emptyList(), + ) : LunaticSubCommand(plugin) { + override val literal = "status" + + override fun build(): LiteralArgumentBuilder = Commands.literal(literal) + } + + private val plugin = mockk(relaxed = true) + + private fun sourceWith( + permission: String, + granted: Boolean, + ): CommandSourceStack = + mockk().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())) + } + + @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))) + } +} -- cgit v1.2.1 From b04e76be78398b5739e27cc0cc606a6c1ac8c232 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:20:25 +0900 Subject: refactor: express command outcomes as message keys Eighty-nine call sites spelled out the same four-line nest to say "fail with localized message X": CommandResult.Failure( MessageFormatter.formatError( languageManager.getMessage("channel.ban.noPermission"), ), ) The pairing of formatError with getMessage was a convention held together only by copy-paste, so changing how command errors are presented meant touching all eighty-nine. fail() and ok() on LunaticCommandBase own that pairing now, and the call sites read as the intent: fail("channel.ban.noPermission"). Three when(error) blocks turned out to map every arm to the same message, and ChannelCreateCommand ran two parallel whens over one error to pick a key and its parameters separately. Both were only visible once the noise around them was gone. Co-Authored-By: Claude --- .../paper/command/core/LunaticCommandBase.kt | 26 +++++-- .../lunaticChat/paper/command/impl/ReplyCommand.kt | 21 ++---- .../lunaticChat/paper/command/impl/TellCommand.kt | 39 ++-------- .../paper/command/impl/lc/ChannelCommand.kt | 2 +- .../paper/command/impl/lc/LunaticChatCommand.kt | 2 +- .../paper/command/impl/lc/SettingsCommand.kt | 2 +- .../paper/command/impl/lc/StatusCommand.kt | 2 +- .../command/impl/lc/channel/ChannelBanCommand.kt | 82 ++++++---------------- .../impl/lc/channel/ChannelCreateCommand.kt | 37 +++------- .../impl/lc/channel/ChannelDeleteCommand.kt | 35 +++------ .../command/impl/lc/channel/ChannelInfoCommand.kt | 24 ++----- .../impl/lc/channel/ChannelInviteCommand.kt | 82 ++++++---------------- .../command/impl/lc/channel/ChannelJoinCommand.kt | 81 ++++++--------------- .../command/impl/lc/channel/ChannelKickCommand.kt | 78 +++++--------------- .../command/impl/lc/channel/ChannelLeaveCommand.kt | 25 ++----- .../command/impl/lc/channel/ChannelListCommand.kt | 8 +-- .../command/impl/lc/channel/ChannelModCommand.kt | 68 ++++-------------- .../impl/lc/channel/ChannelOwnershipCommand.kt | 68 ++++-------------- .../impl/lc/channel/ChannelStatusCommand.kt | 8 +-- .../impl/lc/channel/ChannelSwitchCommand.kt | 49 ++++--------- .../command/impl/lc/channel/ChannelUnbanCommand.kt | 57 ++++----------- .../command/impl/lcv/VelocityStatusCommand.kt | 2 +- 22 files changed, 209 insertions(+), 589 deletions(-) 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 index 596d70d..89b9acc 100644 --- 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 @@ -4,6 +4,7 @@ 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 @@ -23,17 +24,32 @@ abstract class LunaticCommandBase( 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 = 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 = 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 CommandResult.Failure( - MessageFormatter.formatError( - plugin.languageManager.getMessage("general.playerOnlyCommand"), - ), - ) + return fail("general.playerOnlyCommand") } return null 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 b3a1981..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 @@ -36,7 +36,7 @@ class ChannelCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "channel" override val permissionNode = LunaticChatPermissionNode.Channel 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 bd9ca9b..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 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 e8f66d4..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 @@ -27,7 +27,7 @@ import io.papermc.paper.command.brigadier.Commands class SettingsCommand( plugin: LunaticChat, private val settingHandlerRegistry: SettingHandlerRegistry, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "settings" override val permissionNode = LunaticChatPermissionNode.Settings 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 f47285e..526f1d3 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 @@ -20,7 +20,7 @@ import net.kyori.adventure.text.format.NamedTextColor class StatusCommand( plugin: LunaticChat, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, private val configuration: LunaticChatConfiguration, ) : LunaticSubCommand(plugin) { override val literal = "status" 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 c459ca0..a372b26 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 @@ -27,7 +27,7 @@ class ChannelBanCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "ban" override val permissionNode = LunaticChatPermissionNode.ChannelBan @@ -72,20 +72,12 @@ class ChannelBanCommand( val channelId = channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.noActiveChannel"), - ), - ) + ?: return fail("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"), - ), - ) + return fail("channel.ban.noPermission") } // Find target player @@ -93,13 +85,9 @@ class ChannelBanCommand( // 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), - ), - ), + return fail( + "channel.ban.playerNotFound", + mapOf("player" to playerName), ) } @@ -107,23 +95,15 @@ class ChannelBanCommand( // Check if banning self if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.cannotBanSelf"), - ), - ) + return fail("channel.ban.cannotBanSelf") } // 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), - ), - ), + return fail( + "channel.ban.cannotBanBypass", + mapOf("player" to onlineTargetPlayer.name), ) } @@ -149,50 +129,30 @@ class ChannelBanCommand( // Broadcast ban notification to remaining members notificationHandler.broadcastBan(channelId, playerName, sender.name) - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.ban.success", - mapOf("player" to playerName, "channel" to channelName), - ), - ), + ok( + "channel.ban.success", + mapOf("player" to playerName, "channel" to channelName), ) }, onFailure = { error -> when (error) { is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.error"), - ), - ) + fail("channel.ban.error") } is ChannelPlayerBypassBanException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ban.cannotBanBypass", - mapOf("player" to playerName), - ), - ), + fail( + "channel.ban.cannotBanBypass", + mapOf("player" to playerName), ) } is ChannelPlayerAlreadyBannedException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.ban.alreadyBanned", - mapOf("player" to playerName), - ), - ), + fail( + "channel.ban.alreadyBanned", + mapOf("player" to playerName), ) } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ban.error"), - ), - ) + fail("channel.ban.error") } } }, 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 83382c6..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 @@ -21,7 +21,7 @@ import io.papermc.paper.command.brigadier.Commands class ChannelCreateCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "create" override val permissionNode = LunaticChatPermissionNode.ChannelCreate @@ -89,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), ) } @@ -130,25 +126,12 @@ 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)) + } }, ) } 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 6ac6c00..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 @@ -12,7 +12,6 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext 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 @@ -20,7 +19,7 @@ import io.papermc.paper.command.brigadier.Commands class ChannelDeleteCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "delete" override val permissionNode = LunaticChatPermissionNode.ChannelDelete @@ -79,40 +78,24 @@ 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") } } }, 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 a9b464b..43d4515 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 @@ -21,7 +21,7 @@ import org.bukkit.Bukkit class ChannelInfoCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { companion object { private const val MAX_MEMBERS_DISPLAY = 10 @@ -69,33 +69,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 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 096d80c..e645967 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 @@ -25,7 +25,7 @@ class ChannelInviteCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "invite" override val permissionNode = LunaticChatPermissionNode.ChannelInvite @@ -64,53 +64,33 @@ class ChannelInviteCommand( // Get sender's active channel val channelId = channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.noActiveChannel"), - ), - ) + ?: return fail("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"), - ), - ) + return fail("channel.invite.noPermission") } // Find target player val targetPlayer = Bukkit.getPlayer(playerName) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.invite.playerNotFound", - mapOf("player" to playerName), - ), - ), + ?: return fail( + "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"), - ), - ) + return fail("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 fail( + "channel.invite.playerBanned", + mapOf("player" to targetPlayer.name), ) } @@ -132,50 +112,30 @@ class ChannelInviteCommand( ), ) - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.invite.success", - mapOf("player" to targetPlayer.name, "channel" to channelName), - ), - ), + ok( + "channel.invite.success", + mapOf("player" to targetPlayer.name, "channel" to channelName), ) }, onFailure = { error -> when (error) { is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.error"), - ), - ) + fail("channel.invite.error") } is ChannelMemberLimitExceededException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.invite.channelFull", - mapOf("limit" to error.limit.toString()), - ), - ), + fail( + "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), - ), - ), + fail( + "channel.invite.playerBanned", + mapOf("player" to targetPlayer.name), ) } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.invite.error"), - ), - ) + fail("channel.invite.error") } } }, 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 119e89a..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 @@ -20,7 +20,6 @@ import dev.m1sk9.lunaticChat.paper.command.core.CommandContext 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 @@ -30,7 +29,7 @@ class ChannelJoinCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "join" override val permissionNode = LunaticChatPermissionNode.ChannelJoin @@ -76,89 +75,53 @@ 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") } } }, 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 9443e5f..7aae219 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,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.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -25,7 +24,7 @@ class ChannelKickCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "kick" override val permissionNode = LunaticChatPermissionNode.ChannelKick @@ -72,20 +71,12 @@ class ChannelKickCommand( // Get sender's active channel val channelId = channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.noActiveChannel"), - ), - ) + ?: return fail("channel.kick.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.kick.noPermission"), - ), - ) + return fail("channel.kick.noPermission") } // Find target player @@ -93,13 +84,9 @@ class ChannelKickCommand( // 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), - ), - ), + return fail( + "channel.kick.playerNotFound", + mapOf("player" to playerName), ) } @@ -107,36 +94,24 @@ class ChannelKickCommand( // Check if kicking self if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.kick.cannotKickSelf"), - ), - ) + return fail("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), - ), - ), + return fail( + "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), - ), - ), + return fail( + "channel.kick.notMember", + mapOf("player" to playerName), ) } @@ -167,32 +142,13 @@ class ChannelKickCommand( // Broadcast kick notification to remaining members notificationHandler.broadcastKick(channelId, playerName, sender.name) - CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.kick.success", - mapOf("player" to playerName, "channel" to channelName), - ), - ), + ok( + "channel.kick.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"), - ), - ) - } - } + fail("channel.kick.error") }, ) } 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 e329ca3..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 @@ -12,7 +12,6 @@ import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext 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 @@ -22,7 +21,7 @@ class ChannelLeaveCommand( private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "leave" override val permissionNode = LunaticChatPermissionNode.ChannelLeave @@ -52,30 +51,18 @@ 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") } } }, 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 949f6cc..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 @@ -23,7 +23,7 @@ import kotlin.math.ceil class ChannelListCommand( plugin: LunaticChat, private val channelManager: ChannelManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { companion object { private const val CHANNELS_PER_PAGE = 10 @@ -183,11 +183,7 @@ class ChannelListCommand( CommandResult.Success }, onFailure = { error -> - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.list.error"), - ), - ) + fail("channel.list.error") }, ) } 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 8d9be52..5a6ada8 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,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.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -23,7 +22,7 @@ class ChannelModCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "mod" override val permissionNode = LunaticChatPermissionNode.ChannelMod @@ -69,20 +68,12 @@ class ChannelModCommand( // Get sender's active channel val channelId = channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.noActiveChannel"), - ), - ) + ?: return fail("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"), - ), - ) + return fail("channel.mod.noPermission") } // Find target player @@ -90,13 +81,9 @@ class ChannelModCommand( // 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), - ), - ), + return fail( + "channel.mod.playerNotFound", + mapOf("player" to playerName), ) } @@ -104,23 +91,15 @@ class ChannelModCommand( // Check if modding self if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.mod.cannotModSelf"), - ), - ) + return fail("channel.mod.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), - ), - ), + return fail( + "channel.mod.notMember", + mapOf("player" to playerName), ) } @@ -157,32 +136,13 @@ 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") }, ) } 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 20928bf..3236db4 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,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.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -23,7 +22,7 @@ class ChannelOwnershipCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "ownership" override val permissionNode = LunaticChatPermissionNode.ChannelOwnership @@ -70,20 +69,12 @@ class ChannelOwnershipCommand( // Get sender's active channel val channelId = channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.noActiveChannel"), - ), - ) + ?: return fail("channel.ownership.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.ownership.noPermission"), - ), - ) + return fail("channel.ownership.noPermission") } // Find target player @@ -91,13 +82,9 @@ class ChannelOwnershipCommand( // 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), - ), - ), + return fail( + "channel.ownership.playerNotFound", + mapOf("player" to playerName), ) } @@ -105,23 +92,15 @@ class ChannelOwnershipCommand( // Check if transferring to self if (targetPlayerId == sender.uniqueId) { - return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.ownership.cannotTransferToSelf"), - ), - ) + return fail("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), - ), - ), + return fail( + "channel.ownership.notMember", + mapOf("player" to playerName), ) } @@ -144,32 +123,13 @@ 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") }, ) } 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 2da2bb8..bd0f66f 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 @@ -26,7 +26,7 @@ class ChannelStatusCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { companion object { private const val MAX_MEMBERS_DISPLAY = 10 @@ -55,11 +55,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 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 974fd38..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 @@ -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.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,7 +22,7 @@ class ChannelSwitchCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "switch" override val permissionNode = LunaticChatPermissionNode.ChannelSwitch @@ -66,54 +65,34 @@ 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") } } }, 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 1d89b06..43aeaf2 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 @@ -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.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 import org.bukkit.Bukkit @@ -24,7 +23,7 @@ class ChannelUnbanCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, - private val languageManager: LanguageManager, + override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { override val literal = "unban" override val permissionNode = LunaticChatPermissionNode.ChannelUnban @@ -68,20 +67,12 @@ class ChannelUnbanCommand( // Get sender's active channel val channelId = channelManager.getPlayerChannel(sender.uniqueId) - ?: return CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.noActiveChannel"), - ), - ) + ?: return fail("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"), - ), - ) + return fail("channel.unban.noPermission") } // Find target player @@ -89,13 +80,9 @@ class ChannelUnbanCommand( // 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), - ), - ), + return fail( + "channel.unban.playerNotFound", + mapOf("player" to playerName), ) } @@ -108,40 +95,24 @@ class ChannelUnbanCommand( 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), - ), - ), + ok( + "channel.unban.success", + mapOf("player" to playerName, "channel" to channelName), ) }, onFailure = { error -> when (error) { is ChannelNotFoundException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.error"), - ), - ) + fail("channel.unban.error") } is ChannelPlayerNotBannedException -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage( - "channel.unban.playerNotBanned", - mapOf("player" to playerName), - ), - ), + fail( + "channel.unban.playerNotBanned", + mapOf("player" to playerName), ) } else -> { - CommandResult.Failure( - MessageFormatter.formatError( - languageManager.getMessage("channel.unban.error"), - ), - ) + fail("channel.unban.error") } } }, 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") -- cgit v1.2.1 From 232ce55f187d7ec0cea2037d8d285d60465e51ff Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:24:41 +0900 Subject: refactor: describe a setting once, on its SettingKey The three setting handlers were the same 53-line class three times over, differing in one copy() field, one read, and two message keys. Adding persistence, auditing or a permission check to settings meant writing it three times, and a fourth setting meant a fourth copy. A setting is its key, the messages that report it, and how it is read from and written to PlayerChatSettings - so SettingKey now carries all of that, and SettingHandler is the single mechanism that applies it. The registry keeps its role as the seam where features decide which settings exist, which is why registration is still conditional in LunaticChat. SettingHandlerTest now asserts over every SettingKey rather than repeating five near-identical tests per handler, so a new setting is covered the moment it is declared. It also pins down the property that made the old duplication dangerous: writing one setting must not disturb the others. Co-Authored-By: Claude --- .../dev/m1sk9/lunaticChat/paper/LunaticChat.kt | 38 ++-- .../paper/command/setting/SettingHandler.kt | 43 +++-- .../paper/command/setting/SettingKey.kt | 47 ++++- .../handler/ChannelMessageNoticeSettingHandler.kt | 53 ------ .../handler/DirectMessageNoticeSettingHandler.kt | 53 ------ .../handler/JapaneseConversionSettingHandler.kt | 53 ------ .../command/setting/SettingHandlerRegistryTest.kt | 16 +- .../paper/command/setting/SettingHandlerTest.kt | 106 ++++++++++++ .../command/setting/handler/SettingHandlerTest.kt | 191 --------------------- 9 files changed, 196 insertions(+), 404 deletions(-) delete mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt delete mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt delete mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt create mode 100644 platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingHandlerTest.kt delete mode 100644 platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/SettingHandlerTest.kt 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 e006299..f0bbc2c 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 @@ -10,10 +10,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 @@ -109,31 +108,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/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 */ - 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 */ - 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 */ - 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/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(relaxed = true), mockk(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(relaxed = true) + val settingsManager = mockk(relaxed = true) + val languageManager = mockk(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(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(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()) } + } + + @Test + fun `showStatus reports the current value with the status message`() = + eachKey { key, f -> + assertIs(f.handler.showStatus(f.ctx)) + + val player = f.ctx.requirePlayer() + verify { f.languageManager.getMessage(key.statusMessageKey, mapOf("toggle" to "ON")) } + verify { player.sendMessage(any()) } + } + + @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 { - val player = TestUtils.createMockPlayer(uuid = testUUID, name = "TestPlayer") - val ctx = mockk(relaxed = true) - every { ctx.requirePlayer() } returns player - - val settingsManager = mockk(relaxed = true) - every { settingsManager.getSettings(testUUID) } returns - TestUtils.createTestPlayerSettings(uuid = testUUID) - - val languageManager = mockk(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(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(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(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(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()) } - } - - // --- DirectMessageNoticeSettingHandler --- - - @Test - fun `DirectMessageNoticeSettingHandler key should be Notice`() { - val (_, settingsManager, languageManager) = createDependencies() - val handler = DirectMessageNoticeSettingHandler(settingsManager, languageManager) - assertIs(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(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(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(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()) } - } - - // --- ChannelMessageNoticeSettingHandler --- - - @Test - fun `ChannelMessageNoticeSettingHandler key should be ChNotice`() { - val (_, settingsManager, languageManager) = createDependencies() - val handler = ChannelMessageNoticeSettingHandler(settingsManager, languageManager) - assertIs(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(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(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(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()) } - } -} -- cgit v1.2.1 From bf62ebe35653492c123c729ae03cb32d8e20e2e6 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:27:51 +0900 Subject: refactor: single-source the plugin messaging channel and dedup cache The channel Paper and Velocity talk over was declared in seven places, in two spellings ("lunaticchat:main" and the namespace/name pair), one of them an inline literal in CrossServerChatManager that bypassed even its own file's constant. Renaming it meant finding all seven; missing one leaves both sides compiling and starting, just not talking. It now lives next to the codec that defines the wire format. The echo-suppression cache was likewise written twice, and the copies had already drifted in style - one hand-rolled the expiry sweep, the other used filter/map - while staying semantically identical. Any future change to eviction would have had to land in both, and CrossServerChatManager's copy carried a comment claiming ConcurrentHashMap iterators cannot remove(), which they can. MessageDeduplicationCache documents the one property that surprised the tests written against it: eviction orders by millisecond timestamp, so a burst inside a single millisecond evicts arbitrarily among its members. Co-Authored-By: Claude --- .../engine/protocol/PluginMessageChannel.kt | 16 ++++ .../paper/velocity/CrossServerChatManager.kt | 86 ++-------------------- .../velocity/CrossServerDirectMessageManager.kt | 54 ++------------ .../paper/velocity/MessageDeduplicationCache.kt | 70 ++++++++++++++++++ .../paper/velocity/VelocityConnectionManager.kt | 3 +- .../velocity/MessageDeduplicationCacheTest.kt | 44 +++++++++++ .../velocity/messaging/CrossServerChatRelay.kt | 3 +- .../messaging/CrossServerDirectMessageRelay.kt | 3 +- .../velocity/messaging/PluginMessageHandler.kt | 3 +- .../velocity/presence/PresenceTracker.kt | 3 +- 10 files changed, 154 insertions(+), 131 deletions(-) create mode 100644 engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageChannel.kt create mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCache.kt create mode 100644 platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/MessageDeduplicationCacheTest.kt 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/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..70ee6d4 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,13 @@ 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 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 +25,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() + private val processedMessages = MessageDeduplicationCache(cacheSize, logger, "global chat") /** * Sends a global chat message to Velocity for cross-server broadcast @@ -51,7 +44,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 +65,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 +77,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 +90,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 +112,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) } @@ -153,54 +133,4 @@ class CrossServerChatManager( 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() - 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() + 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() + + /** + * 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/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(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/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) } /** -- cgit v1.2.1 From 288d1e4a4babc136e8b3837d19970cc37023eb8e Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:36:39 +0900 Subject: refactor: move channel moderation rules out of the commands The rule "owners and moderators may kick and ban" was written out six times, as `senderRole == null || senderRole == ChannelRole.MEMBER`, while ChannelMembershipManager.hasRole - which encodes the OWNER > MODERATOR > MEMBER hierarchy in one place and has tests - had no production caller at all. Letting moderators ban meant editing six files and hoping none was missed; a miss is a silent privilege change. The bypass and self-invite rules had drifted further still: the engine defines ChannelPlayerBypassBanException, ChannelPlayerBypassKickException and ChannelCannotInviteSelfException, but nothing threw them. The checks lived inline in the commands, and ChannelBanCommand carried a catch arm for an exception that could never arrive. Those three rules now live on ChannelMembershipManager and throw what the engine already declared, so every caller is held to them rather than only the command path. ChannelInviteCommand also pre-checked for a banned target, which joinChannel checks again a moment later and reports through the same message; the pre-check is gone. What stays in the commands is what belongs there: parsing an argument and choosing which message to show. ChannelSubCommand names the steps they share and derives message keys from the subcommand's own literal. The tests followed the rules: bypass and self-invite are now asserted against ChannelMembershipManager, with a bypass predicate injected so the manager stays testable without a running server. Co-Authored-By: Claude --- .../paper/chat/channel/ChannelMembershipManager.kt | 69 +++++++++++++++ .../command/impl/lc/channel/ChannelBanCommand.kt | 99 +++++----------------- .../impl/lc/channel/ChannelInviteCommand.kt | 75 +++------------- .../command/impl/lc/channel/ChannelKickCommand.kt | 98 ++++++--------------- .../command/impl/lc/channel/ChannelModCommand.kt | 50 +++-------- .../impl/lc/channel/ChannelOwnershipCommand.kt | 48 +++-------- .../command/impl/lc/channel/ChannelSubCommand.kt | 61 +++++++++++++ .../command/impl/lc/channel/ChannelUnbanCommand.kt | 59 +++---------- .../chat/channel/ChannelMembershipManagerTest.kt | 93 +++++++++++++++++++- .../impl/lc/channel/ChannelBanCommandTest.kt | 15 ++-- .../impl/lc/channel/ChannelInviteCommandTest.kt | 20 +++-- .../impl/lc/channel/ChannelKickCommandTest.kt | 13 ++- .../impl/lc/channel/ChannelModCommandTest.kt | 10 +-- .../impl/lc/channel/ChannelOwnershipCommandTest.kt | 10 +-- .../impl/lc/channel/ChannelUnbanCommandTest.kt | 7 +- 15 files changed, 354 insertions(+), 373 deletions(-) create mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt 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. @@ -263,6 +280,58 @@ class ChannelMembershipManager( return Result.success(Unit) } + /** + * 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 { + 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 { + 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 { + if (actorId == targetId) { + return Result.failure(ChannelCannotInviteSelfException(actorId)) + } + return joinChannel(targetId, channelId, bypassPrivateCheck = true) + } + /** * Gets all channels where the player is a member. * 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 a372b26..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 @@ -14,7 +13,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -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,11 +22,11 @@ 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, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "ban" override val permissionNode = LunaticChatPermissionNode.ChannelBan @@ -70,90 +68,35 @@ class ChannelBanCommand( ): CommandResult { val sender = ctx.requirePlayer() - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.ban.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 fail("channel.ban.noPermission") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotBanSelf") - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) + val targetName = target.name ?: playerName - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.ban.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if banning self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.ban.cannotBanSelf") - } - - // Check if target has bypass permission - val onlineTargetPlayer = Bukkit.getPlayer(playerName) - if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { - return fail( - "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 + val channelName = channelNameOf(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), - ), + Bukkit.getPlayer(playerName)?.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.ban.wasBanned", + mapOf("channel" to channelName, "banner" to sender.name), ), - ) - } - - // Broadcast ban notification to remaining members + ), + ) notificationHandler.broadcastBan(channelId, playerName, sender.name) - ok( - "channel.ban.success", - mapOf("player" to playerName, "channel" to channelName), - ) + okHere("success", mapOf("player" to playerName, "channel" to channelName)) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - fail("channel.ban.error") - } - is ChannelPlayerBypassBanException -> { - fail( - "channel.ban.cannotBanBypass", - mapOf("player" to playerName), - ) - } - is ChannelPlayerAlreadyBannedException -> { - fail( - "channel.ban.alreadyBanned", - mapOf("player" to playerName), - ) - } - else -> { - fail("channel.ban.error") - } + is ChannelPlayerBypassBanException -> failHere("cannotBanBypass", mapOf("player" to targetName)) + is ChannelPlayerAlreadyBannedException -> failHere("alreadyBanned", mapOf("player" to playerName)) + else -> failHere("error") } }, ) 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 e645967..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,8 +4,8 @@ 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 @@ -13,7 +13,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -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 @@ -23,10 +22,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelInviteCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "invite" override val permissionNode = LunaticChatPermissionNode.ChannelInvite override val aliases = listOf("inv") @@ -61,46 +60,17 @@ class ChannelInviteCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.invite.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 fail("channel.invite.noPermission") - } - - // Find target player val targetPlayer = Bukkit.getPlayer(playerName) - ?: return fail( - "channel.invite.playerNotFound", - mapOf("player" to playerName), - ) - - // Check if inviting self - if (targetPlayer.uniqueId == sender.uniqueId) { - return fail("channel.invite.cannotInviteSelf") - } + ?: return failHere("playerNotFound", mapOf("player" to playerName)) - // Check if player is banned - val isBanned = channelManager.isPlayerBanned(channelId, targetPlayer.uniqueId).getOrElse { false } - if (isBanned) { - return fail( - "channel.invite.playerBanned", - mapOf("player" to targetPlayer.name), - ) - } - - // 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( @@ -112,31 +82,14 @@ class ChannelInviteCommand( ), ) - ok( - "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 -> { - fail("channel.invite.error") - } - is ChannelMemberLimitExceededException -> { - fail( - "channel.invite.channelFull", - mapOf("limit" to error.limit.toString()), - ) - } - is ChannelPlayerBannedException -> { - fail( - "channel.invite.playerBanned", - mapOf("player" to targetPlayer.name), - ) - } - else -> { - fail("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") } }, ) 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 7aae219..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,6 +4,7 @@ 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.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -11,7 +12,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -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 @@ -21,11 +21,11 @@ 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, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "kick" override val permissionNode = LunaticChatPermissionNode.ChannelKick override val aliases = listOf("k") @@ -68,87 +68,45 @@ class ChannelKickCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("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 fail("channel.kick.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 fail( - "channel.kick.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if kicking self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.kick.cannotKickSelf") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotKickSelf") - // Check if target has bypass permission - val onlineTargetPlayer = Bukkit.getPlayer(playerName) - if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { - return fail( - "channel.kick.cannotKickBypass", - mapOf("player" to onlineTargetPlayer.name), - ) - } + val targetId = target.uniqueId + val targetName = target.name ?: playerName - // Check if target is a member - val isMember = membershipManager.isMember(targetPlayerId, channelId).getOrElse { false } - if (!isMember) { - return fail( - "channel.kick.notMember", - mapOf("player" to playerName), - ) + if (!membershipManager.isMember(targetId, channelId).getOrElse { false }) { + return failHere("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) + if (channelManager.getPlayerChannel(targetId) == channelId) { + channelManager.setPlayerChannel(targetId, null) } - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId + val channelName = channelNameOf(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), - ), + Bukkit.getPlayer(playerName)?.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.kick.wasKicked", + mapOf("channel" to channelName, "kicker" to sender.name), ), - ) - } - - // Broadcast kick notification to remaining members + ), + ) notificationHandler.broadcastKick(channelId, playerName, sender.name) - ok( - "channel.kick.success", - mapOf("player" to playerName, "channel" to channelName), - ) + okHere("success", mapOf("player" to playerName, "channel" to channelName)) }, onFailure = { error -> - fail("channel.kick.error") + when (error) { + is ChannelPlayerBypassKickException -> failHere("cannotKickBypass", mapOf("player" to targetName)) + else -> failHere("error") + } }, ) } 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 5a6ada8..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 @@ -10,7 +10,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -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 @@ -20,10 +19,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelModCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "mod" override val permissionNode = LunaticChatPermissionNode.ChannelMod @@ -65,43 +64,16 @@ class ChannelModCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.mod.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 fail("channel.mod.noPermission") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotModSelf") - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.mod.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if modding self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.mod.cannotModSelf") - } - - // Check if target is a member - val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) - if (targetRole == null) { - return fail( - "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 = 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 3236db4..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 @@ -10,7 +10,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -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 @@ -20,10 +19,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelOwnershipCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "ownership" override val permissionNode = LunaticChatPermissionNode.ChannelOwnership override val aliases = listOf("own") @@ -66,42 +65,15 @@ class ChannelOwnershipCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("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 fail("channel.ownership.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 fail( - "channel.ownership.playerNotFound", - mapOf("player" to playerName), - ) - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotTransferToSelf") - val targetPlayerId = targetPlayer.uniqueId - - // Check if transferring to self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.ownership.cannotTransferToSelf") - } - - // Check if target is a member - val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) - if (targetRole == null) { - return fail( - "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 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..0075970 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt @@ -0,0 +1,61 @@ +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 org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import org.bukkit.entity.Player +import java.util.UUID + +/** + * 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..`. */ + protected fun failHere( + suffix: String, + args: Map = emptyMap(), + ): CommandResult = fail("channel.$literal.$suffix", args) + + /** A success carrying `channel..`. */ + protected fun okHere( + suffix: String, + args: Map = 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/ChannelUnbanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt index 43aeaf2..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,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.ChannelPlayerNotBannedException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -12,7 +11,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands @@ -21,10 +19,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelUnbanCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "unban" override val permissionNode = LunaticChatPermissionNode.ChannelUnban @@ -64,56 +62,19 @@ class ChannelUnbanCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.unban.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 fail("channel.unban.noPermission") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.unban.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // 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 - - ok( - "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 -> { - fail("channel.unban.error") - } - is ChannelPlayerNotBannedException -> { - fail( - "channel.unban.playerNotBanned", - mapOf("player" to playerName), - ) - } - else -> { - fail("channel.unban.error") - } + is ChannelPlayerNotBannedException -> failHere("playerNotBanned", mapOf("player" to playerName)) + else -> failHere("error") } }, ) 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 = emptySet(), ): Triple { val logger = TestUtils.TestLogger() val storage = mockk(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(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(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(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(result.exceptionOrNull()) + } } 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()) } 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(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()) } 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..65db127 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,9 @@ 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.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) 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.inviteToChannel(testUUID, targetUUID, channelId) } returns Result.success(Unit) every { deps.channelManager.getChannel(channelId) } returns Result.success(channel) mockkStatic(Bukkit::class) @@ -76,11 +77,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()) } returns selfTarget + every { deps.membershipManager.inviteToChannel(testUUID, testUUID, channelId) } returns + Result.failure(ChannelCannotInviteSelfException(testUUID)) val result = deps.command.execute(deps.ctx, "Player1") @@ -96,8 +99,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 +120,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 +140,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()) } 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(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()) } 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(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(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 -- cgit v1.2.1 From 7e7d0875087185e6c68bb29f1d60bd51b300bfef Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:38:18 +0900 Subject: refactor: extract the debounced async save YamlPlayerSettingsStorage and ConversionCache each carried a line-for-line identical AtomicBoolean-plus-runDelayed debounce, five-second constant included, so changing the save cadence meant changing it twice and noticing that it was written twice. Extracting it also removed the only reason those two classes held a JavaPlugin: they took the whole plugin to reach the scheduler. They now take the collaborator they actually use, which is both narrower and testable without a running server. ChannelStorage is deliberately left alone - it saves through runNow with no debounce at all, and giving it one would change when writes happen. Co-Authored-By: Claude --- .../dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt | 36 ++++++++++++++++++++++ .../m1sk9/lunaticChat/paper/ServiceInitializer.kt | 4 +-- .../lunaticChat/paper/converter/ConversionCache.kt | 23 ++------------ .../paper/settings/YamlPlayerSettingsStorage.kt | 21 +++---------- 4 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt 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/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt index 5ed8ed6..6213348 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 @@ -169,7 +169,7 @@ class ServiceInitializer( val storage = YamlPlayerSettingsStorage( settingsFile = settingsFile, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) @@ -194,7 +194,7 @@ class ServiceInitializer( ConversionCache( cacheFile = plugin.dataFolder.resolve(configuration.features.japaneseConversion.cacheFilePath).toPath(), maxEntries = configuration.features.japaneseConversion.cacheMaxEntries, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) cache.loadFromDisk() 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() - 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/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt index 5513303..7a727b0 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,10 +2,8 @@ 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 @@ -16,16 +14,15 @@ import kotlin.io.path.writeText * 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. @@ -72,16 +69,6 @@ class YamlPlayerSettingsStorage( * @param data The settings data to save */ fun queueAsyncSave(data: PlayerSettingsData) { - if (saveFlag.compareAndSet(false, true)) { - plugin.server.asyncScheduler.runDelayed( - plugin, - { - saveFlag.set(false) - saveToDisk(data) - }, - 5, - TimeUnit.SECONDS, - ) - } + saver.request { saveToDisk(data) } } } -- cgit v1.2.1 From bc8010bc1b5401f11c80b565ba9a4de7d969a0a9 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:42:01 +0900 Subject: refactor: let each service live in exactly one place ServiceInitializer held ten nullable fields alongside the ServiceContainer it returns. Six of them were written and never read; the remaining four made shutdown() ambiguous, reading conversionCache and channelMessageLogger from its own fields but everything else from the container it was handed. LunaticChat then mirrored seven more into public vars, one of which (channelMessageHandler) nothing read at all. Now the container is the single place a service lives: the initializer builds and returns, shutdown and the periodic task read from what they are given, and the plugin's public properties delegate rather than copy. A new service is one field instead of three, and no copy can go stale. PlayerSettingsManager had the same shape at a smaller scale: three UUID maps plus a PlayerSettingsData kept in sync by hand, where the data object was a pure derivation rebuilt - three full map copies - on every toggle. It now keeps one map and derives the snapshot at save time, which also closes the window where queueAsyncSave captured state that changes before the debounce fires. Co-Authored-By: Claude --- .../dev/m1sk9/lunaticChat/paper/LunaticChat.kt | 26 +++----- .../m1sk9/lunaticChat/paper/ServiceContainer.kt | 6 ++ .../m1sk9/lunaticChat/paper/ServiceInitializer.kt | 77 +++++++++------------- .../paper/settings/PlayerSettingsManager.kt | 65 +++++++++--------- 4 files changed, 77 insertions(+), 97 deletions(-) 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 f0bbc2c..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 @@ -30,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 @@ -71,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() 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 6213348..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, ) } @@ -188,7 +180,7 @@ class ServiceInitializer( * - Google IME API client * - Romanji converter */ - private fun initializeJapaneseConversion(): RomanjiConverter { + private fun initializeJapaneseConversion(): Pair { // Initialize conversion cache val cache = ConversionCache( @@ -198,7 +190,6 @@ class ServiceInitializer( 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 { 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/settings/PlayerSettingsManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt index f0927d8..de35c55 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() - private val directMessageNotificationCache = ConcurrentHashMap() - private val channelMessageNotificationCache = ConcurrentHashMap() - private lateinit var settingsData: PlayerSettingsData + private val settings = ConcurrentHashMap() + + // 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 }, + ) } -- cgit v1.2.1 From 7bcaaf9a305c2a8608a420c8f05521bd2de089dd Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 19:45:33 +0900 Subject: refactor: fold the remaining small duplications - MessageFormatter built the same prefix component in three functions. - LanguageManager copied kaml's YamlNode into a private YamlValue tree before flattening it, so the map case was written twice and the list-of-maps case rendered a Kotlin data class toString into a player facing string. It now folds YamlNode directly. - StatusCommand inlined `if (enabled) "toggle.on" else "toggle.off"`, which is the body of LanguageManager.getToggleText. - The three chat formats each spelled out their own chain of String.replace, with the valid placeholder names documented only in a config.yml comment. - ChannelContext carried a channelId that both construction sites filled with channel.id; it is now derived, so the two cannot disagree. - ChannelInfo and ChannelStatus each declared MAX_MEMBERS_DISPLAY = 10 and built the same truncated member line, differing only in indent. A divergence between the two constants would have been invisible. Co-Authored-By: Claude --- .../engine/chat/channel/ChannelContext.kt | 9 +- .../engine/chat/channel/ChannelDataClassesTest.kt | 6 +- .../paper/chat/channel/ChannelManager.kt | 2 - .../paper/chat/handler/ChannelMessageHandler.kt | 10 +- .../paper/chat/handler/DirectMessageHandler.kt | 10 +- .../paper/command/impl/lc/StatusCommand.kt | 2 +- .../command/impl/lc/channel/ChannelInfoCommand.kt | 27 +---- .../impl/lc/channel/ChannelStatusCommand.kt | 35 +------ .../command/impl/lc/channel/ChannelSubCommand.kt | 36 +++++++ .../dev/m1sk9/lunaticChat/paper/i18n/ChatFormat.kt | 10 ++ .../lunaticChat/paper/i18n/LanguageManager.kt | 110 +++++---------------- .../lunaticChat/paper/i18n/MessageFormatter.kt | 37 ++----- .../paper/velocity/CrossServerChatManager.kt | 10 +- 13 files changed, 106 insertions(+), 198 deletions(-) create mode 100644 platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/i18n/ChatFormat.kt 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, -) +) { + /** 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/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/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/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/impl/lc/StatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt index 526f1d3..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 @@ -164,7 +164,7 @@ 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) 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 43d4515..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 @@ -23,10 +23,6 @@ class ChannelInfoCommand( private val channelManager: ChannelManager, override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { - companion object { - private const val MAX_MEMBERS_DISPLAY = 10 - } - override val literal = "info" override val permissionNode = LunaticChatPermissionNode.ChannelInfo override val aliases = listOf("i") @@ -129,28 +125,7 @@ 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) 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 bd0f66f..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 @@ -28,10 +28,6 @@ class ChannelStatusCommand( private val membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, ) : LunaticSubCommand(plugin) { - companion object { - private const val MAX_MEMBERS_DISPLAY = 10 - } - override val literal = "status" override val permissionNode = LunaticChatPermissionNode.ChannelStatus override val aliases = listOf("st") @@ -104,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) } 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 index 0075970..b48c879 100644 --- 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 @@ -6,11 +6,47 @@ 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, + 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`. * 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 = + 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,26 +1,13 @@ 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, - ) : YamlValue() - - data class ListValue( - val value: List, - ) : YamlValue() -} - /** * Manages language files and provides message retrieval with string-based keys. * @@ -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 = - when (node) { - is com.charleskorn.kaml.YamlMap -> { - val result = mutableMapOf() - 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() - 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, - prefix: String = "", - ): Map { - val result = mutableMapOf() - - 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, + ) { + 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/velocity/CrossServerChatManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt index 70ee6d4..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 @@ -4,6 +4,7 @@ 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 @@ -126,10 +127,11 @@ 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) } -- cgit v1.2.1 From 16789f7ad3aadac756904ffc8bda0a7c6271a273 Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 20:06:20 +0900 Subject: fix: let a debounced settings write see changes made while it waits DebouncedSaver keeps only the first callback of a burst, and queueAsyncSave closed over the snapshot taken when it was called. So if one player toggled a setting and a second toggled two seconds later, the write that fired at five seconds persisted the first snapshot and dropped the second player's change - it survived in memory until some later toggle happened to trigger another write, and was lost on a crash. Passing a supplier instead means the snapshot is taken when the write runs, which is what "batched into a single save" was always meant to mean. ConversionCache already had this shape by passing ::saveToDisk. The staleness predates the refactor, but bc8010b claimed to have closed this window; it only moved where the snapshot was built, not when. Co-Authored-By: Claude --- .../paper/settings/PlayerSettingsManager.kt | 2 +- .../paper/settings/YamlPlayerSettingsStorage.kt | 8 +++++--- .../paper/settings/PlayerSettingsManagerTest.kt | 23 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) 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 de35c55..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 @@ -61,7 +61,7 @@ class PlayerSettingsManager( */ fun updateSettings(settings: PlayerChatSettings) { this.settings[settings.uuid] = settings - storage.queueAsyncSave(snapshot()) + storage.queueAsyncSave(::snapshot) logger.fine("Updated settings for player ${settings.uuid}") } 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 7a727b0..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 @@ -66,9 +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) { - saver.request { saveToDisk(data) } + fun queueAsyncSave(data: () -> PlayerSettingsData) { + saver.request { saveToDisk(data()) } } } 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 @@ -81,6 +82,28 @@ class PlayerSettingsManagerTest { verify(exactly = 1) { storage.queueAsyncSave(any()) } } + @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) -- cgit v1.2.1 From f813fc8c0e2ff48c1dc087733d8d250797a2cd3c Mon Sep 17 00:00:00 2001 From: Sho Sakuma Date: Sun, 2 Aug 2026 20:49:25 +0900 Subject: test: drop a stub the invite command no longer reaches ChannelInviteCommand used to pre-check isPlayerBanned itself. That check moved into ChannelMembershipManager.inviteToChannel in 288d1e4, but the stub stayed behind, implying the command still consults the channel manager for ban state when it no longer does. Co-Authored-By: Claude --- .../paper/command/impl/lc/channel/ChannelInviteCommandTest.kt | 1 - 1 file changed, 1 deletion(-) 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 65db127..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 @@ -55,7 +55,6 @@ class ChannelInviteCommandTest { every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) - every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(false) every { deps.membershipManager.inviteToChannel(testUUID, targetUUID, channelId) } returns Result.success(Unit) every { deps.channelManager.getChannel(channelId) } returns Result.success(channel) -- cgit v1.2.1