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