summaryrefslogtreecommitdiff
path: root/platform-paper
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-26 02:58:23 +0900
committerGitHub <noreply@github.com>2026-01-26 02:58:23 +0900
commit18a4bbc4ba1a6766af920fb0849215f058ff260e (patch)
treec570633f29f2398cb263e2eb90a7d56f0ea41e78 /platform-paper
parent0fb8f4c92ad5dd7d20778de862b4d12f67f71d5e (diff)
parentc7714add1934baf55a5e6268b1a70780eaa84fea (diff)
downloadLunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.tar.gz
LunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.tar.bz2
LunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.zip
Merge pull request #69 from m1sk9/feat-core-channel-chat
feat: Core Channel Features (Phase 1)
Diffstat (limited to 'platform-paper')
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt27
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt14
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt106
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt103
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt98
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt288
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt224
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt99
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt80
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt (renamed from platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt)41
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt9
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt11
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt183
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt84
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt51
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt142
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt102
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt132
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt85
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt127
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt193
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt130
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt71
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt8
-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.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt24
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt14
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt5
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt9
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt23
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt73
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt71
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt6
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt4
-rw-r--r--platform-paper/src/main/resources/config.yml6
-rw-r--r--platform-paper/src/main/resources/languages/en.yml102
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml102
-rw-r--r--platform-paper/src/main/resources/paper-plugin.yml20
45 files changed, 2823 insertions, 117 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
index 6525b55..d238dd6 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
@@ -1,11 +1,16 @@
package dev.m1sk9.lunaticChat.paper
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+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.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
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.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.common.UpdateCheckResult
@@ -27,6 +32,10 @@ class LunaticChat :
// Public API - accessed by commands (maintain backward compatibility)
lateinit var directMessageHandler: DirectMessageHandler
lateinit var languageManager: LanguageManager
+ var channelManager: ChannelManager? = null
+ var channelMembershipManager: ChannelMembershipManager? = null
+ var chatModeManager: ChatModeManager? = null
+ var channelMessageHandler: ChannelMessageHandler? = null
// Private services
private lateinit var services: ServiceContainer
@@ -60,6 +69,10 @@ class LunaticChat :
// Set public API properties (for command access)
directMessageHandler = services.directMessageHandler
languageManager = services.languageManager
+ channelManager = services.channelManager
+ channelMembershipManager = services.channelMembershipManager
+ chatModeManager = services.chatModeManager
+ channelMessageHandler = services.channelMessageHandler
// Schedule periodic tasks
serviceInitializer.schedulePeriodicTasks()
@@ -96,6 +109,16 @@ class LunaticChat :
),
)
+ // 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) {
settingHandlerRegistry.register(
@@ -113,7 +136,7 @@ class LunaticChat :
)
// Conditionally register /reply command if quick replies are enabled
- if (configuration.features.quickRepliesEnabled.enabled) {
+ if (configuration.features.quickReplies.enabled) {
commandRegistry.registerAll(
ReplyCommand(this, services.directMessageHandler, services.languageManager),
)
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 6cd700b..3cd9e73 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
@@ -1,6 +1,10 @@
package dev.m1sk9.lunaticChat.paper
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+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.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
@@ -15,10 +19,18 @@ import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
* @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 channelManager Optional (only when channel chat feature is enabled)
+ * @property channelMembershipManager Optional (only when channel chat feature is enabled)
+ * @property chatModeManager Optional (only when channel chat feature is enabled)
+ * @property channelMessageHandler Optional (only when channel chat feature is enabled)
*/
data class ServiceContainer(
val languageManager: LanguageManager,
val playerSettingsManager: PlayerSettingsManager,
val directMessageHandler: DirectMessageHandler,
val romajiConverter: RomanjiConverter? = null,
+ val channelManager: ChannelManager? = null,
+ val channelMembershipManager: ChannelMembershipManager? = null,
+ val chatModeManager: ChatModeManager? = null,
+ val channelMessageHandler: ChannelMessageHandler? = 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 e39f04f..35f70d8 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
@@ -1,7 +1,13 @@
package dev.m1sk9.lunaticChat.paper
import dev.m1sk9.lunaticChat.engine.converter.GoogleIMEClient
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeStorage
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelStorage
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.ConversionCache
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
@@ -14,6 +20,16 @@ import java.util.logging.Logger
import kotlin.time.Duration.Companion.milliseconds
/**
+ * Container for channel-related components.
+ */
+private data class ChannelComponents(
+ val channelManager: ChannelManager,
+ val channelMembershipManager: ChannelMembershipManager,
+ val chatModeManager: ChatModeManager,
+ val channelMessageHandler: ChannelMessageHandler,
+)
+
+/**
* Handles initialization and shutdown of all plugin services.
*
* This class centralizes service initialization logic, ensuring proper
@@ -26,6 +42,10 @@ class ServiceInitializer(
private val logger: Logger,
) {
private var conversionCache: ConversionCache? = null
+ private var channelManager: ChannelManager? = null
+ private var channelMembershipManager: ChannelMembershipManager? = null
+ private var chatModeManager: ChatModeManager? = null
+ private var channelMessageHandler: ChannelMessageHandler? = null
/**
* Initializes all services in dependency order.
@@ -34,7 +54,8 @@ class ServiceInitializer(
* 1. LanguageManager (required by all features)
* 2. PlayerSettingsManager (required for DM notifications)
* 3. Japanese Conversion (optional, config-dependent)
- * 4. DirectMessageHandler (depends on settings manager and romaji converter)
+ * 4. ChannelStorage
+ * 5. DirectMessageHandler (depends on settings manager and romaji converter)
*
* @return ServiceContainer with all initialized services
*/
@@ -60,7 +81,19 @@ class ServiceInitializer(
null
}
- // 4. Initialize handlers
+ // 4. Initialize channel manager, membership manager, chat mode manager, and channel message handler
+ val channelComponents =
+ if (configuration.features.channelChat.enabled) {
+ initializeChannelManager(playerSettingsManager)
+ } else {
+ null
+ }
+ val channelManager = channelComponents?.channelManager
+ val channelMembershipManager = channelComponents?.channelMembershipManager
+ val chatModeManager = channelComponents?.chatModeManager
+ val channelMessageHandler = channelComponents?.channelMessageHandler
+
+ // 5. Initialize handlers
val directMessageHandler =
DirectMessageHandler(
settingsManager = playerSettingsManager,
@@ -72,6 +105,10 @@ class ServiceInitializer(
playerSettingsManager = playerSettingsManager,
directMessageHandler = directMessageHandler,
romajiConverter = romajiConverter,
+ channelManager = channelManager,
+ channelMembershipManager = channelMembershipManager,
+ chatModeManager = chatModeManager,
+ channelMessageHandler = channelMessageHandler,
)
}
@@ -136,6 +173,67 @@ class ServiceInitializer(
}
/**
+ * Initializes channel manager, membership manager, chat mode manager, and channel message handler with storage.
+ */
+ private fun initializeChannelManager(settingsManager: PlayerSettingsManager): ChannelComponents {
+ val channelsFile = plugin.dataFolder.resolve("channels.json").toPath()
+ val storage =
+ ChannelStorage(
+ channelsFile = channelsFile,
+ plugin = plugin,
+ logger = logger,
+ )
+
+ val manager =
+ ChannelManager(
+ storage = storage,
+ logger = logger,
+ )
+ manager.initialize()
+ channelManager = manager
+
+ val membershipManager =
+ ChannelMembershipManager(
+ channelManager = manager,
+ logger = logger,
+ )
+ channelMembershipManager = membershipManager
+
+ val chatModeFile = plugin.dataFolder.resolve("chatmodes.json").toPath()
+ val chatModeStorage =
+ ChatModeStorage(
+ dataFile = chatModeFile,
+ logger = logger,
+ )
+
+ val chatMode =
+ ChatModeManager(
+ storage = chatModeStorage,
+ logger = logger,
+ )
+ chatMode.initialize()
+ chatModeManager = chatMode
+
+ val messageHandler =
+ ChannelMessageHandler(
+ settingsManager = settingsManager,
+ channelManager = manager,
+ logger =
+ io.ktor.util.logging
+ .KtorSimpleLogger("ChannelMessageHandler"),
+ )
+ channelMessageHandler = messageHandler
+
+ logger.info("Channel manager, membership manager, chat mode manager, and channel message handler initialized successfully.")
+ return ChannelComponents(
+ channelManager = manager,
+ channelMembershipManager = membershipManager,
+ chatModeManager = chatMode,
+ channelMessageHandler = messageHandler,
+ )
+ }
+
+ /**
* Schedules periodic tasks such as cache saving.
*/
fun schedulePeriodicTasks() {
@@ -158,5 +256,7 @@ class ServiceInitializer(
fun shutdown(services: ServiceContainer) {
services.playerSettingsManager.saveToDisk()
conversionCache?.saveToDisk()
+ services.channelManager?.saveToDisk()
+ services.chatModeManager?.shutdown()
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt
new file mode 100644
index 0000000..93a7f04
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt
@@ -0,0 +1,103 @@
+package dev.m1sk9.lunaticChat.paper.chat
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.engine.chat.ChatModeData
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.logging.Logger
+
+/**
+ * Manages player chat modes with persistence.
+ *
+ * Chat modes determine where player messages are sent by default.
+ * Modes are persisted across server restarts.
+ *
+ * @property storage Chat mode storage layer
+ * @property logger Logger for operations
+ */
+class ChatModeManager(
+ private val storage: ChatModeStorage,
+ private val logger: Logger,
+) {
+ private val chatModes = ConcurrentHashMap<UUID, ChatMode>()
+
+ /**
+ * Initializes the manager by loading data from storage.
+ */
+ fun initialize() {
+ val data = storage.loadFromDisk()
+ chatModes.putAll(data.modes)
+ logger.info("ChatModeManager initialized with ${chatModes.size} saved modes")
+ }
+
+ /**
+ * Gets a player's current chat mode.
+ *
+ * @param playerId The player's UUID
+ * @return The player's chat mode, or DEFAULT if not set
+ */
+ fun getChatMode(playerId: UUID): ChatMode = chatModes.getOrDefault(playerId, ChatMode.Companion.DEFAULT)
+
+ /**
+ * Sets a player's chat mode.
+ *
+ * @param playerId The player's UUID
+ * @param mode The chat mode to set
+ */
+ fun setChatMode(
+ playerId: UUID,
+ mode: ChatMode,
+ ) {
+ chatModes[playerId] = mode
+ saveToStorage()
+ }
+
+ /**
+ * Toggles a player's chat mode between GLOBAL and CHANNEL.
+ *
+ * @param playerId The player's UUID
+ * @return The new chat mode after toggling
+ */
+ fun toggleChatMode(playerId: UUID): ChatMode {
+ val currentMode = getChatMode(playerId)
+ val newMode = currentMode.toggle()
+ setChatMode(playerId, newMode)
+ return newMode
+ }
+
+ /**
+ * Removes a player's chat mode setting (reverts to default).
+ *
+ * @param playerId The player's UUID
+ */
+ fun removeChatMode(playerId: UUID) {
+ chatModes.remove(playerId)
+ saveToStorage()
+ }
+
+ /**
+ * Saves current state to storage asynchronously.
+ */
+ private fun saveToStorage() {
+ val data = ChatModeData(modes = chatModes.toMap())
+ storage.queueAsyncSave(data)
+ }
+
+ /**
+ * Forces a synchronous save to storage.
+ * Should only be called during plugin shutdown.
+ */
+ fun saveToDisk() {
+ val data = ChatModeData(modes = chatModes.toMap())
+ storage.saveToDisk(data)
+ }
+
+ /**
+ * Shuts down the chat mode manager and its storage executor.
+ * Should be called during plugin disable to prevent thread leaks.
+ */
+ fun shutdown() {
+ saveToDisk()
+ storage.shutdown()
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt
new file mode 100644
index 0000000..77fcdb3
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt
@@ -0,0 +1,98 @@
+package dev.m1sk9.lunaticChat.paper.chat
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatModeData
+import dev.m1sk9.lunaticChat.engine.exception.ChatModeStorageException
+import kotlinx.serialization.json.Json
+import java.nio.file.Path
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.logging.Logger
+import kotlin.io.path.exists
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+
+/**
+ * Handles JSON storage for chat mode data.
+ * Provides async save queue and synchronous save for shutdown.
+ *
+ * @property dataFile Path to chatmodes.json
+ * @property logger Logger for operations
+ */
+class ChatModeStorage(
+ private val dataFile: Path,
+ private val logger: Logger,
+) {
+ private val json =
+ Json {
+ prettyPrint = true
+ ignoreUnknownKeys = true
+ }
+
+ private val saveExecutor = Executors.newSingleThreadExecutor()
+
+ /**
+ * Loads chat mode data from disk.
+ *
+ * @return ChatModeData loaded from file, or empty data if file doesn't exist
+ * @throws ChatModeStorageException if loading fails
+ */
+ fun loadFromDisk(): ChatModeData {
+ if (!dataFile.exists()) {
+ logger.info("Chat mode data file not found, starting with empty data")
+ return ChatModeData()
+ }
+
+ return try {
+ val jsonContent = dataFile.readText()
+ json.decodeFromString<ChatModeData>(jsonContent)
+ } catch (e: Exception) {
+ throw ChatModeStorageException("Failed to load chat mode data from ${dataFile.fileName}", e)
+ }
+ }
+
+ /**
+ * Queues an asynchronous save operation.
+ *
+ * @param data ChatModeData to save
+ */
+ fun queueAsyncSave(data: ChatModeData) {
+ saveExecutor.submit {
+ try {
+ saveToDisk(data)
+ } catch (e: Exception) {
+ logger.severe("Failed to save chat mode data: ${e.message}")
+ }
+ }
+ }
+
+ /**
+ * Saves chat mode data to disk synchronously.
+ *
+ * @param data ChatModeData to save
+ * @throws ChatModeStorageException if saving fails
+ */
+ fun saveToDisk(data: ChatModeData) {
+ try {
+ val jsonContent = json.encodeToString(data)
+ dataFile.writeText(jsonContent)
+ } catch (e: Exception) {
+ throw ChatModeStorageException("Failed to save chat mode data to ${dataFile.fileName}", e)
+ }
+ }
+
+ /**
+ * Shuts down the async save executor.
+ * Should be called during plugin disable.
+ */
+ fun shutdown() {
+ saveExecutor.shutdown()
+ try {
+ if (!saveExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ saveExecutor.shutdownNow()
+ }
+ } catch (e: InterruptedException) {
+ saveExecutor.shutdownNow()
+ Thread.currentThread().interrupt()
+ }
+ }
+}
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
new file mode 100644
index 0000000..fc8d6da
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt
@@ -0,0 +1,288 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelContext
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMember
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.logging.Logger
+import kotlin.collections.forEach
+
+class ChannelManager(
+ private val storage: ChannelStorage,
+ private val logger: Logger,
+) {
+ private val channelsCache = ConcurrentHashMap<String, Channel>()
+ private val membersCache = ConcurrentHashMap<String, CopyOnWriteArrayList<ChannelMember>>()
+ private val activeChannels = ConcurrentHashMap<UUID, String>()
+
+ /**
+ * Initializes the ChannelManager by loading data from storage.
+ */
+ fun initialize() {
+ val data = storage.loadFromDisk()
+ channelsCache.putAll(data.channels)
+ data.members.forEach { (channelId, members) ->
+ membersCache[channelId] = CopyOnWriteArrayList(members)
+ }
+ data.activeChannels.forEach { (playerIdStr, channelId) ->
+ try {
+ val playerId = UUID.fromString(playerIdStr)
+ activeChannels[playerId] = channelId
+ } catch (e: IllegalArgumentException) {
+ logger.warning("Invalid UUID in activeChannels: $playerIdStr")
+ }
+ }
+ logger.info("ChannelManager initialized with ${channelsCache.size} channels and ${activeChannels.size} active channels.")
+ }
+
+ /**
+ * Creates a new channel.
+ *
+ * @param channel The channel to create.
+ * @return Result containing the created channel or an error if the channel already exists.
+ * @throws ChannelNotFoundException if a channel with the same ID already exists.
+ */
+ fun createChannel(channel: Channel): Result<Channel> {
+ if (channelsCache.containsKey(channel.id)) {
+ return Result.failure(ChannelNotFoundException(channel.id))
+ }
+
+ channelsCache[channel.id] = channel
+
+ val ownerMember =
+ ChannelMember(
+ channelId = channel.id,
+ playerId = channel.ownerId,
+ role = ChannelRole.OWNER,
+ )
+ membersCache[channel.id] = CopyOnWriteArrayList(listOf(ownerMember))
+
+ // Set the owner's active channel
+ setPlayerChannel(channel.ownerId, channel.id)
+
+ saveToStorage()
+ logger.info("Created new channel with ID ${channel.id}.")
+ return Result.success(channel)
+ }
+
+ /**
+ * Deletes a channel.
+ *
+ * @param channelId The ID of the channel to delete.
+ * @param requesterId The ID of the player requesting the deletion.
+ * @return Result indicating success or failure of the deletion.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun deleteChannel(
+ channelId: String,
+ requesterId: UUID,
+ ): Result<Unit> {
+ val channel =
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ if (channel.ownerId != requesterId) {
+ return Result.failure(ChannelNoOwnerPermissionException(requesterId))
+ }
+
+ // Clear active channel for all players who have this channel active
+ activeChannels.entries.removeIf { it.value == channelId }
+
+ channelsCache.remove(channelId)
+ membersCache.remove(channelId)
+
+ saveToStorage()
+ logger.info("Owner with ID $requesterId deleted channel with ID $channelId.")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Retrieves a channel by its ID.
+ *
+ * @param channelId The ID of the channel to retrieve.
+ * @return Result containing the channel or an error if not found.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun getChannel(channelId: String): Result<Channel> {
+ val channel =
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+ return Result.success(channel)
+ }
+
+ /**
+ * Retrieves all channels.
+ *
+ * @return Result containing the list of all channels.
+ */
+ fun getAllChannels(): Result<List<Channel>> = Result.success(channelsCache.values.toList())
+
+ /**
+ * Retrieves all public channels.
+ *
+ * @return Result containing the list of public channels.
+ */
+ fun getPublicChannels(): Result<List<Channel>> {
+ val channels =
+ channelsCache.values
+ .filter { !it.isPrivate }
+ .sortedBy { it.name }
+ return Result.success(channels)
+ }
+
+ /**
+ * Retrieves members of a channel.
+ *
+ * @param channelId The ID of the channel.
+ * @return Result containing the list of channel members or an error if the channel is not found.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun getChannelMembers(channelId: String): Result<List<ChannelMember>> {
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val members = membersCache[channelId]?.toList() ?: emptyList()
+ return Result.success(members)
+ }
+
+ /**
+ * Adds a member to a channel.
+ *
+ * @param channelId The ID of the channel.
+ * @param playerId The UUID of the player to add.
+ * @param role The role of the new member.
+ * @return Result indicating success or failure of the operation.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun addMember(
+ channelId: String,
+ playerId: UUID,
+ role: ChannelRole,
+ ): Result<Unit> {
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val members =
+ membersCache.getOrPut(channelId) {
+ CopyOnWriteArrayList()
+ }
+ val newMember =
+ ChannelMember(
+ channelId = channelId,
+ playerId = playerId,
+ role = role,
+ joinedAt = System.currentTimeMillis(),
+ )
+
+ members.add(newMember)
+ saveToStorage()
+
+ return Result.success(Unit)
+ }
+
+ /**
+ * Removes a member from a channel.
+ *
+ * @param channelId The ID of the channel.
+ * @param playerId The UUID of the player to remove.
+ * @return Result indicating success or failure of the operation.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun removeMember(
+ channelId: String,
+ playerId: UUID,
+ ): Result<Unit> {
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val members =
+ membersCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val removed = members.removeIf { it.playerId == playerId }
+ if (!removed) {
+ return Result.failure(ChannelNotFoundException(channelId))
+ }
+
+ saveToStorage()
+ return Result.success(Unit)
+ }
+
+ /**
+ * Saves the current state of channels and members to storage asynchronously.
+ */
+ private fun saveToStorage() {
+ val data =
+ ChannelData(
+ channels = channelsCache.toMap(),
+ members = membersCache.mapValues { it.value.toList() },
+ activeChannels = activeChannels.mapKeys { it.key.toString() },
+ )
+ storage.queueAsyncSave(data)
+ logger.fine("${channelsCache.size} channels queued for saving to storage.")
+ }
+
+ /**
+ * Saves the current state of channels and members to storage synchronously.
+ * Should only br called during server shutdown.
+ */
+ fun saveToDisk() {
+ val data =
+ ChannelData(
+ channels = channelsCache.toMap(),
+ members = membersCache.mapValues { it.value.toList() },
+ activeChannels = activeChannels.mapKeys { it.key.toString() },
+ )
+ storage.saveToDisk(data)
+ }
+
+ /**
+ * Gets the active channel of a player.
+ *
+ * @param playerId The UUID of the player.
+ * @return The ID of the active channel or null if none is set.
+ */
+ fun getPlayerChannel(playerId: UUID): String? = activeChannels[playerId]
+
+ /**
+ * Gets the full channel context (channel and members) of a player's active channel.
+ *
+ * @param playerId The UUID of the player.
+ * @return The ChannelContext of the active channel or null if none is set.
+ */
+ fun getPlayerChannelContext(playerId: UUID): ChannelContext? {
+ val channelId = activeChannels[playerId] ?: return null
+ val channel = channelsCache[channelId] ?: return null
+ val members = membersCache[channelId]?.toList() ?: return null
+
+ return ChannelContext(
+ channelId = channelId,
+ channel = channel,
+ members = members,
+ )
+ }
+
+ /**
+ * Sets the active channel of a player.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel to set as active, or null to clear the active channel.
+ */
+ fun setPlayerChannel(
+ playerId: UUID,
+ channelId: String?,
+ ) {
+ if (channelId == null) {
+ activeChannels.remove(playerId)
+ } else {
+ activeChannels[playerId] = channelId
+ }
+ saveToStorage()
+ }
+}
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
new file mode 100644
index 0000000..6d6ad8d
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt
@@ -0,0 +1,224 @@
+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.ChannelMemberAlreadyException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelRuntimeException
+import java.util.UUID
+import java.util.logging.Logger
+
+class ChannelMembershipManager(
+ private val channelManager: ChannelManager,
+ private val logger: Logger,
+) {
+ /**
+ * Checks if a player is a member of a channel.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @return Result containing true if the player is a member, false otherwise.
+ */
+ fun isMember(
+ playerId: UUID,
+ channelId: String,
+ ): Result<Boolean> =
+ channelManager.getChannelMembers(channelId).map { members ->
+ members.any { it.playerId == playerId }
+ }
+
+ /**
+ * Gets the role of a member in a channel.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @return Result containing the ChannelRole of the member.
+ * @throws ChannelNotMemberException if the player is not a member of the channel.
+ */
+ fun getMemberRole(
+ playerId: UUID,
+ channelId: String,
+ ): Result<ChannelRole> =
+ channelManager.getChannelMembers(channelId).mapCatching { members ->
+ members
+ .find {
+ it.playerId == playerId
+ }?.role ?: throw ChannelNotMemberException(playerId, channelId)
+ }
+
+ /**
+ * Checks if a player has a specific role or higher in a channel.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @param requireRole The required ChannelRole.
+ * @return Result containing true if the player has the required role or higher, false otherwise.
+ */
+ fun hasRole(
+ playerId: UUID,
+ channelId: String,
+ requireRole: ChannelRole,
+ ): Result<Boolean> =
+ getMemberRole(playerId, channelId).fold(
+ onSuccess = { playerRole ->
+ val hasRequiredRole =
+ when (requireRole) {
+ ChannelRole.MEMBER -> true
+ ChannelRole.MODERATOR -> playerRole in setOf(ChannelRole.MODERATOR, ChannelRole.OWNER)
+ ChannelRole.OWNER -> playerRole == ChannelRole.OWNER
+ }
+ Result.success(hasRequiredRole)
+ },
+ onFailure = { Result.success(false) },
+ )
+
+ /**
+ * Adds a player to a channel as a member and sets it as their active channel.
+ * If the player is already a member of other channels, they remain members of those channels.
+ * Only one channel can be active at a time.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @return Result indicating success or failure.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ * @throws ChannelMemberAlreadyException if the player is already a member of this channel.
+ * @throws ChannelRuntimeException for other runtime errors.
+ */
+ fun joinChannel(
+ playerId: UUID,
+ channelId: String,
+ ): Result<Unit> {
+ // Check if channel exists
+ val channel =
+ channelManager.getChannel(channelId).getOrElse {
+ return Result.failure(
+ ChannelNotFoundException(channelId),
+ )
+ }
+
+ // Check if this channel is already active
+ val currentActiveChannel = channelManager.getPlayerChannel(playerId)
+ if (currentActiveChannel == channelId) {
+ return Result.failure(
+ ChannelAlreadyActiveException(playerId, channelId),
+ )
+ }
+
+ // Check if player is already a member
+ val isAlreadyMember =
+ isMember(playerId, channelId).getOrElse {
+ return Result.failure(
+ ChannelRuntimeException("Failed to check membership for player $playerId in channel $channelId", it),
+ )
+ }
+
+ // If already a member, return error
+ if (isAlreadyMember) {
+ return Result.failure(
+ ChannelMemberAlreadyException(playerId, channelId),
+ )
+ }
+
+ // Add as member
+ channelManager.addMember(channelId, playerId, ChannelRole.MEMBER).getOrElse {
+ return Result.failure(it)
+ }
+
+ // Set as active channel
+ channelManager.setPlayerChannel(playerId, channelId)
+ logger.info("Player $playerId joined channel $channelId")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Clears the player's active channel without removing them from channel membership.
+ * The player remains a member of the channel and can rejoin by using the join command.
+ *
+ * @param playerId The UUID of the player.
+ * @return Result indicating success or failure.
+ * @throws ChannelNotMemberException if the player does not have an active channel.
+ */
+ fun leaveChannel(playerId: UUID): Result<Unit> {
+ val currentChannel =
+ channelManager.getPlayerChannel(playerId)
+ ?: return Result.failure(
+ ChannelNotMemberException(playerId, "no active channel"),
+ )
+
+ // Clear active channel
+ channelManager.setPlayerChannel(playerId, null)
+ logger.info("Player $playerId left active channel $currentChannel (still a member)")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Switches the player's active channel to a channel they are already a member of.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel to switch to.
+ * @return Result indicating success or failure.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ * @throws ChannelNotMemberException if the player is not a member of the channel.
+ */
+ fun switchChannel(
+ playerId: UUID,
+ channelId: String,
+ ): Result<Unit> {
+ // Check if channel exists
+ val channel =
+ channelManager.getChannel(channelId).getOrElse {
+ return Result.failure(
+ ChannelNotFoundException(channelId),
+ )
+ }
+
+ // Check if this channel is already active
+ val currentActiveChannel = channelManager.getPlayerChannel(playerId)
+ if (currentActiveChannel == channelId) {
+ return Result.failure(
+ ChannelAlreadyActiveException(playerId, channelId),
+ )
+ }
+
+ // Check if player is a member
+ val isAlreadyMember =
+ isMember(playerId, channelId).getOrElse {
+ return Result.failure(
+ ChannelRuntimeException("Failed to check membership for player $playerId in channel $channelId", it),
+ )
+ }
+
+ if (!isAlreadyMember) {
+ return Result.failure(
+ ChannelNotMemberException(playerId, channelId),
+ )
+ }
+
+ // Set as active channel
+ channelManager.setPlayerChannel(playerId, channelId)
+ logger.info("Player $playerId switched to channel $channelId")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Gets all channels where the player is a member.
+ *
+ * @param playerId The UUID of the player.
+ * @return Result containing a list of channel IDs where the player is a member.
+ */
+ fun getPlayerChannels(playerId: UUID): Result<List<String>> {
+ val allChannels =
+ channelManager.getAllChannels().getOrElse {
+ return Result.failure(it)
+ }
+
+ val playerChannels =
+ allChannels
+ .filter { channel ->
+ isMember(playerId, channel.id).getOrElse { false }
+ }.map { it.id }
+
+ return Result.success(playerChannels)
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt
new file mode 100644
index 0000000..7176014
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt
@@ -0,0 +1,99 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData
+import dev.m1sk9.lunaticChat.engine.exception.ChannelStorageLoadException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelStorageSaveException
+import kotlinx.serialization.json.Json
+import org.bukkit.plugin.java.JavaPlugin
+import java.nio.file.Path
+import java.util.logging.Logger
+import kotlin.io.path.bufferedReader
+import kotlin.io.path.exists
+import kotlin.io.path.writeText
+
+/**
+ * Manages the storage of channel data on disk.
+ *
+ * @property channelsFile The path to the file where channel data is stored.
+ * @property plugin The JavaPlugin instance for accessing plugin resources.
+ * @property logger The logger for logging messages.
+ */
+class ChannelStorage(
+ private val channelsFile: Path,
+ private val plugin: JavaPlugin,
+ private val logger: Logger,
+) {
+ private val json =
+ Json {
+ prettyPrint = true
+ ignoreUnknownKeys = true
+ }
+
+ /**
+ * Loads channel data from disk.
+ *
+ * @return The loaded ChannelData.
+ * @throws ChannelStorageLoadException if there is an error loading the data.
+ */
+ fun loadFromDisk(): ChannelData {
+ if (!channelsFile.exists()) {
+ logger.warning("Channel storage not found, will create a new one.")
+ return ChannelData()
+ }
+
+ return try {
+ val jsonContent =
+ channelsFile.bufferedReader().use {
+ it.readText()
+ }
+ json.decodeFromString(ChannelData.serializer(), jsonContent).also {
+ logger.info("Successfully loaded channels from ${channelsFile.fileName}.")
+ }
+ } catch (e: Exception) {
+ throw ChannelStorageLoadException(
+ "Failed to load channels from ${channelsFile.fileName}: ${e.message}",
+ e,
+ )
+ }
+ }
+
+ /**
+ * Saves channel data to disk.
+ *
+ * @param data The ChannelData to save.
+ * @throws ChannelStorageSaveException if there is an error saving the data.
+ */
+ fun saveToDisk(data: ChannelData) {
+ try {
+ val jsonContent = json.encodeToString(ChannelData.serializer(), data)
+ channelsFile.writeText(jsonContent).also {
+ logger.fine("Successfully saved channels from ${channelsFile.fileName}.")
+ }
+ } catch (e: Exception) {
+ throw ChannelStorageSaveException(
+ "Failed to save channels to ${channelsFile.fileName}: ${e.message}",
+ e,
+ )
+ }
+ }
+
+ /**
+ * Queues an asynchronous save of channel data to disk.
+ *
+ * @param data The ChannelData to save.
+ * @throws ChannelStorageSaveException if there is an error saving the data.
+ */
+ fun queueAsyncSave(data: ChannelData) {
+ plugin.server.scheduler.runTaskAsynchronously(
+ plugin,
+ Runnable {
+ try {
+ saveToDisk(data)
+ } catch (e: ChannelStorageSaveException) {
+ logger.severe("Error saving channel data asynchronously: ${e.message}")
+ e.printStackTrace()
+ }
+ },
+ )
+ }
+}
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
new file mode 100644
index 0000000..70756d9
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
@@ -0,0 +1,80 @@
+package dev.m1sk9.lunaticChat.paper.chat.handler
+
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
+import dev.m1sk9.lunaticChat.paper.common.playChannelReceiveNotification
+import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification
+import dev.m1sk9.lunaticChat.paper.config.ConfigManager
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import io.ktor.util.logging.Logger
+import net.kyori.adventure.text.Component
+import org.bukkit.Bukkit
+import org.bukkit.entity.Player
+
+class ChannelMessageHandler(
+ private val settingsManager: PlayerSettingsManager?,
+ private val channelManager: ChannelManager,
+ private val logger: Logger,
+) {
+ private var lunaticChatConfiguration = ConfigManager.getConfiguration()
+
+ fun sendChannelMessage(
+ player: Player,
+ message: String,
+ ): Boolean {
+ val playerId = player.uniqueId
+ val context =
+ channelManager.getPlayerChannelContext(playerId)
+ ?: return false
+ val formattedMessage = formatChannelMessage(player.name, context.channel.name, message)
+
+ // Play notification sound to sender if enabled
+ settingsManager?.let { manager ->
+ val senderSettings = manager.getSettings(playerId)
+ if (senderSettings.channelMessageNotificationEnabled) {
+ player.playMessageSendNotification()
+ }
+ }
+
+ SpyPermissionManager
+ .getDirectMessageSpyPlayers()
+ .values
+ .filter { it.isOnline && it.uniqueId != playerId }
+ .forEach { it.sendMessage(formattedMessage) }
+ context.members.forEach { member ->
+ Bukkit.getPlayer(member.playerId)?.let { memberPlayer ->
+ if (memberPlayer.isOnline) {
+ memberPlayer.sendMessage(formattedMessage)
+
+ // Play notification sound to receiver if enabled and not the sender
+ if (memberPlayer.uniqueId != playerId) {
+ settingsManager?.let { manager ->
+ val receiverSettings = manager.getSettings(memberPlayer.uniqueId)
+ if (receiverSettings.channelMessageNotificationEnabled) {
+ memberPlayer.playChannelReceiveNotification()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ logger.info("Channel Message from ${player.name} in ${context.channel.name}: $message")
+ return true
+ }
+
+ private fun formatChannelMessage(
+ senderName: String,
+ channelName: String,
+ message: String,
+ ): Component {
+ val format = lunaticChatConfiguration.messageFormat.channelMessageFormat
+ val text =
+ format
+ .replace("{sender}", senderName)
+ .replace("{channel}", channelName)
+ .replace("{message}", message)
+
+ return Component.text(text)
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
index 4683c08..6155381 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
@@ -1,8 +1,8 @@
-package dev.m1sk9.lunaticChat.paper.command.handler
+package dev.m1sk9.lunaticChat.paper.chat.handler
import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
import dev.m1sk9.lunaticChat.paper.common.playDirectMessageNotification
-import dev.m1sk9.lunaticChat.paper.common.playDirectMessageSendNotification
+import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification
import dev.m1sk9.lunaticChat.paper.config.ConfigManager
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
@@ -59,10 +59,18 @@ class DirectMessageHandler(
/**
* Clears message history for a player (called on disconnect).
+ * Removes entries where this player is either the sender or recipient.
*/
fun clearPlayer(player: Player) {
- lastMessager.remove(player.uniqueId)
- lastRecipient.remove(player.uniqueId)
+ val playerId = player.uniqueId
+
+ // Remove entries where this player is the sender
+ lastMessager.remove(playerId)
+ lastRecipient.remove(playerId)
+
+ // Remove entries where this player is the recipient
+ lastMessager.entries.removeIf { it.value == playerId }
+ lastRecipient.entries.removeIf { it.value == playerId }
}
/**
@@ -72,7 +80,7 @@ class DirectMessageHandler(
*
* @return true if message was sent successfully
*/
- suspend fun sendDirectMessage(
+ fun sendDirectMessage(
sender: Player,
recipient: Player,
message: String,
@@ -82,15 +90,20 @@ class DirectMessageHandler(
val senderSettings = settingsManager?.getSettings(sender.uniqueId)
val recipientSettings = settingsManager?.getSettings(recipient.uniqueId)
+ // Handle romaji conversion if enabled (requires blocking for HTTP call)
val displayMessage =
- senderSettings
- ?.takeIf { it.japaneseConversionEnabled }
- ?.let { romanjiConverter }
- ?.runCatching {
- convert(message)
- ?.let { "$message §e($it)" }
- ?: message
- }?.getOrNull() ?: message
+ if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) {
+ runCatching {
+ kotlinx.coroutines.runBlocking {
+ romanjiConverter
+ ?.convert(message)
+ ?.let { "$message §e($it)" }
+ ?: message
+ }
+ }.getOrNull() ?: message
+ } else {
+ message
+ }
val format = lunaticChatConfiguration.messageFormat.directMessageFormat
@@ -105,7 +118,7 @@ class DirectMessageHandler(
sender.apply {
sendMessage(userMessage)
takeIf { senderSettings?.directMessageNotificationEnabled == true }
- ?.playDirectMessageSendNotification()
+ ?.playMessageSendNotification()
}
recipient.apply {
sendMessage(userMessage)
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 817c002..1fe4835 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
@@ -98,7 +98,7 @@ abstract class LunaticCommand(
if (isPlayerOnly && !ctx.isPlayer) {
return CommandResult.Failure(
MessageFormatter.formatError(
- plugin.languageManager.getMessage("playerOnlyCommand"),
+ plugin.languageManager.getMessage("general.playerOnlyCommand"),
),
)
}
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 af7a32c..e1a997e 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
@@ -5,17 +5,16 @@ 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.chat.handler.DirectMessageHandler
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.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
-import kotlinx.coroutines.runBlocking
@Command(
name = "reply",
@@ -58,13 +57,11 @@ class ReplyCommand(
dmHandler.getReplyTarget(sender)
?: return CommandResult.Failure(
MessageFormatter.formatError(
- languageManager.getMessage("replyTargetNotFound"),
+ languageManager.getMessage("directMessage.replyTargetNotFound"),
),
)
- runBlocking {
- dmHandler.sendDirectMessage(sender, target, message)
- }
+ dmHandler.sendDirectMessage(sender, target, message)
return CommandResult.Success
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
index 1238138..5375b08 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
@@ -7,17 +7,16 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder
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.handler.DirectMessageHandler
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.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
-import kotlinx.coroutines.runBlocking
import org.bukkit.Bukkit
import java.util.concurrent.CompletableFuture
@@ -69,21 +68,19 @@ class TellCommand(
Bukkit.getPlayer(targetName)
?: return CommandResult.Failure(
MessageFormatter.formatError(
- languageManager.getMessage("tellTargetOffline", mapOf("target" to targetName)),
+ languageManager.getMessage("directMessage.targetOffline", mapOf("target" to targetName)),
),
)
if (recipient.uniqueId == sender.uniqueId) {
return CommandResult.Failure(
MessageFormatter.formatError(
- languageManager.getMessage("tellYourself"),
+ languageManager.getMessage("directMessage.yourself"),
),
)
}
- runBlocking {
- directMessageHandler.sendDirectMessage(sender, recipient, message)
- }
+ directMessageHandler.sendDirectMessage(sender, recipient, message)
return CommandResult.Success
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt
new file mode 100644
index 0000000..3e47c7e
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt
@@ -0,0 +1,183 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc
+
+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.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.impl.lc.channel.ChannelCreateCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelDeleteCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelJoinCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelLeaveCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelListCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelStatusCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelSwitchCommand
+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
+
+@PlayerOnly
+class ChannelCommand(
+ 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)
+ }
+
+ @Permission(LunaticChatPermissionNode.Channel::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> {
+ val channelCommand = Commands.literal("channel")
+
+ // Add subcommands
+ channelCommand
+ .then(
+ ChannelCreateCommand(
+ plugin,
+ channelManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelListCommand(
+ plugin,
+ channelManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelJoinCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelLeaveCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelSwitchCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelStatusCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelDeleteCommand(
+ plugin,
+ channelManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+
+ // Default help message when no subcommand is provided
+ channelCommand.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = showHelp(context)
+ handleResult(context, result)
+ }
+
+ return channelCommand
+ }
+
+ private fun showHelp(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ sender.sendMessage(
+ MessageFormatter.format(
+ 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.delete"),
+ ),
+ ),
+ )
+
+ 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/ChatModeCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt
new file mode 100644
index 0000000..73a50e5
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt
@@ -0,0 +1,84 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc
+
+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.chat.ChatModeManager
+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.impl.lc.chatmode.ChatModeToggleCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+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
+
+@PlayerOnly
+class ChatModeCommand(
+ plugin: LunaticChat,
+ private val chatModeManager: ChatModeManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChatMode::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> {
+ val chatModeCommand = Commands.literal("chatmode")
+
+ // Add toggle subcommand
+ chatModeCommand.then(
+ ChatModeToggleCommand(
+ plugin,
+ chatModeManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+
+ // Default behavior: show current chat mode
+ chatModeCommand.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = showCurrentMode(context)
+ handleResult(context, result)
+ }
+
+ return chatModeCommand
+ }
+
+ private fun showCurrentMode(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+ val currentMode = chatModeManager.getChatMode(sender.uniqueId)
+
+ val modeKey =
+ when (currentMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> "chatmode.mode.global"
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> "chatmode.mode.channel"
+ }
+
+ val modeColor =
+ when (currentMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> NamedTextColor.GREEN
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> NamedTextColor.AQUA
+ }
+
+ sender.sendMessage(
+ Component
+ .text(languageManager.getMessage("chatmode.current") + ": ", NamedTextColor.GRAY)
+ .append(Component.text(languageManager.getMessage(modeKey), modeColor)),
+ )
+
+ 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 2d634be..af08089 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
@@ -31,19 +31,48 @@ class LunaticChatCommand(
override val description: String
get() = languageManager.getMessage("commandDescription.lc")
- override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
- Commands
- .literal(name)
- .then(
- SettingsCommand(
- plugin,
- settingHandlerRegistry,
- languageManager,
- ).buildWithPermissionCheck(),
- ).then(
- StatusCommand(
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> {
+ val command =
+ Commands
+ .literal(name)
+ .then(
+ SettingsCommand(
+ plugin,
+ settingHandlerRegistry,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ StatusCommand(
+ plugin,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+
+ // Add channel command if channel manager is available
+ plugin.channelManager?.let { manager ->
+ plugin.channelMembershipManager?.let { membershipManager ->
+ command.then(
+ ChannelCommand(
+ plugin,
+ manager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+ }
+ }
+
+ // Add chatmode command if chat mode manager is available
+ plugin.chatModeManager?.let { chatModeManager ->
+ command.then(
+ ChatModeCommand(
plugin,
+ chatModeManager,
languageManager,
).buildWithPermissionCheck(),
)
+ }
+
+ return command
+ }
}
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 51ddac8..9cd6645 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
@@ -105,7 +105,7 @@ class SettingsCommand(
val availableKeys = settingHandlerRegistry.getAvailableKeys()
val helpMessage =
MessageFormatter.format(
- languageManager.getMessage("settingsAvailableValues", mapOf("values" to availableKeys.joinToString(", "))),
+ languageManager.getMessage("settings.availableValues", mapOf("values" to availableKeys.joinToString(", "))),
)
ctx.reply(helpMessage)
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 b91c8ca..20971ee 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
@@ -48,8 +48,8 @@ class StatusCommand(
sendMessage(
MessageFormatter
.format(
- languageManager.getMessage("statusRunningVersion", mapOf("version" to meta.version)),
- ).hoverEvent(HoverEvent.showText(Component.text(languageManager.getMessage("statusHover")))),
+ languageManager.getMessage("status.runningVersion", mapOf("version" to meta.version)),
+ ).hoverEvent(HoverEvent.showText(Component.text(languageManager.getMessage("status.hover")))),
)
urls.forEach { (label, url) ->
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
new file mode 100644
index 0000000..29da990
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt
@@ -0,0 +1,142 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.BoolArgumentType
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+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.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+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)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelCreate::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("create")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .then(
+ Commands
+ .argument("name", StringArgumentType.string())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val name = StringArgumentType.getString(ctx, "name")
+
+ val result = execute(context, channelId, name, null, false)
+ handleResult(context, result)
+ }.then(
+ Commands
+ .argument("description", StringArgumentType.string())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val name = StringArgumentType.getString(ctx, "name")
+ val description = StringArgumentType.getString(ctx, "description")
+
+ val result = execute(context, channelId, name, description, false)
+ handleResult(context, result)
+ }.then(
+ Commands
+ .argument("isPrivate", BoolArgumentType.bool())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val name = StringArgumentType.getString(ctx, "name")
+ val description = StringArgumentType.getString(ctx, "description")
+ val isPrivate = BoolArgumentType.getBool(ctx, "isPrivate")
+
+ val result = execute(context, channelId, name, description, isPrivate)
+ handleResult(context, result)
+ },
+ ),
+ ),
+ ),
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ name: String,
+ description: String?,
+ isPrivate: Boolean,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ // 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),
+ ),
+ ),
+ )
+ }
+
+ val channel =
+ Channel(
+ id = channelId,
+ name = name,
+ description = description,
+ isPrivate = isPrivate,
+ ownerId = sender.uniqueId,
+ )
+
+ val result = channelManager.createChannel(channel)
+ return result.fold(
+ onSuccess = {
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.create.success",
+ mapOf("name" to name, "id" to channelId),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "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
new file mode 100644
index 0000000..9d39c70
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt
@@ -0,0 +1,102 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException
+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.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+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)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelDelete::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("delete")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+
+ val result = execute(context, channelId)
+ handleResult(context, result)
+ },
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = channelManager.deleteChannel(channelId, sender.uniqueId)
+ return result.fold(
+ onSuccess = {
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "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),
+ ),
+ ),
+ )
+ }
+ is ChannelNoOwnerPermissionException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.delete.noPermission"),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("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/ChannelJoinCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
new file mode 100644
index 0000000..2a73199
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
@@ -0,0 +1,132 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException
+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.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
+
+@PlayerOnly
+class ChannelJoinCommand(
+ 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)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelJoin::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("join")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .suggests { _, builder ->
+ // Tab completion: suggest all public channel IDs
+ val channels = channelManager.getPublicChannels().getOrNull() ?: emptyList()
+ channels.forEach { channel ->
+ builder.suggest(channel.id)
+ }
+ builder.buildFuture()
+ }.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val result = execute(context, channelId)
+ handleResult(context, result)
+ },
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = membershipManager.joinChannel(sender.uniqueId, channelId)
+ return result.fold(
+ onSuccess = {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+
+ // Play notification sound
+ sender.playChannelJoinNotification()
+
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "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),
+ ),
+ ),
+ )
+ }
+ is ChannelAlreadyActiveException -> {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "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)),
+ ),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("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/ChannelLeaveCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
new file mode 100644
index 0000000..299ad47
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
@@ -0,0 +1,85 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+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
+
+@PlayerOnly
+class ChannelLeaveCommand(
+ 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)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelLeave::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("leave").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ // Get current channel before leaving
+ val currentChannelId = channelManager.getPlayerChannel(sender.uniqueId)
+ val currentChannel = currentChannelId?.let { channelManager.getChannel(it).getOrNull() }
+
+ val result = membershipManager.leaveChannel(sender.uniqueId)
+ return result.fold(
+ onSuccess = {
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "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"),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("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
new file mode 100644
index 0000000..aaf05af
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt
@@ -0,0 +1,127 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+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.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.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.event.ClickEvent
+import net.kyori.adventure.text.event.HoverEvent
+import net.kyori.adventure.text.format.NamedTextColor
+import net.kyori.adventure.text.format.TextDecoration
+
+@PlayerOnly
+class ChannelListCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelList::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("list").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = channelManager.getPublicChannels()
+ return result.fold(
+ onSuccess = { channels ->
+ if (channels.isEmpty()) {
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("channel.list.empty"),
+ ),
+ )
+ } else {
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.list.header",
+ mapOf("count" to channels.size.toString()),
+ ),
+ ),
+ )
+
+ channels.forEach { channel ->
+ val memberCountResult = channelManager.getChannelMembers(channel.id)
+ val memberCount =
+ memberCountResult.getOrNull()?.size
+ ?: 0
+
+ val hoverTextBuilder =
+ Component
+ .text()
+ .append(Component.text("ID: ", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+ .append(Component.newline())
+ .append(Component.text("Members: ", NamedTextColor.GRAY))
+ .append(Component.text(memberCount.toString(), NamedTextColor.WHITE))
+
+ channel.description?.let { desc ->
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.text("Description: ", NamedTextColor.GRAY))
+ .append(Component.text(desc, NamedTextColor.WHITE))
+ }
+
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.newline())
+ .append(Component.text(languageManager.getMessage("channel.list.clickToJoin"), NamedTextColor.GREEN))
+ .append(Component.text(" ▶", NamedTextColor.YELLOW))
+
+ val hoverText = hoverTextBuilder.build()
+
+ val channelInfo =
+ Component
+ .text(" • ", NamedTextColor.GRAY)
+ .append(
+ Component
+ .text(channel.name, NamedTextColor.AQUA)
+ .decorate(TextDecoration.BOLD),
+ ).append(Component.text(" (", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+ .append(Component.text(")", NamedTextColor.GRAY))
+ .hoverEvent(HoverEvent.showText(hoverText))
+ .clickEvent(ClickEvent.runCommand("/lc channel join ${channel.id}"))
+
+ sender.sendMessage(channelInfo)
+ }
+ }
+ CommandResult.Success
+ },
+ onFailure = { error ->
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("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/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
new file mode 100644
index 0000000..fa03965
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
@@ -0,0 +1,193 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+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.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 net.kyori.adventure.text.Component
+import net.kyori.adventure.text.event.ClickEvent
+import net.kyori.adventure.text.event.HoverEvent
+import net.kyori.adventure.text.format.NamedTextColor
+import net.kyori.adventure.text.format.TextDecoration
+
+@PlayerOnly
+class ChannelStatusCommand(
+ 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)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelStatus::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("status").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ // Get active channel
+ val activeChannelId = channelManager.getPlayerChannel(sender.uniqueId)
+ val activeChannel = activeChannelId?.let { channelManager.getChannel(it).getOrNull() }
+
+ // Get all player's channels
+ val playerChannelIds =
+ membershipManager.getPlayerChannels(sender.uniqueId).getOrElse {
+ return CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.status.error"),
+ ),
+ )
+ }
+
+ // Display header
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("channel.status.header"),
+ ),
+ )
+
+ // Display active channel
+ if (activeChannel != null) {
+ val activeText =
+ Component
+ .text(" ")
+ .append(
+ Component.text(
+ languageManager.getMessage("channel.status.activeChannel"),
+ NamedTextColor.GREEN,
+ ),
+ ).append(Component.text(": ", NamedTextColor.GRAY))
+ .append(
+ Component
+ .text(activeChannel.name, NamedTextColor.AQUA),
+ ).append(Component.text(" (", NamedTextColor.GRAY))
+ .append(Component.text(activeChannel.id, NamedTextColor.YELLOW))
+ .append(Component.text(")", NamedTextColor.GRAY))
+
+ sender.sendMessage(activeText)
+ } else {
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ Component.text(
+ languageManager.getMessage("channel.status.noActiveChannel"),
+ NamedTextColor.GRAY,
+ ),
+ ),
+ )
+ }
+
+ sender.sendMessage(Component.empty())
+
+ // Display channels list
+ if (playerChannelIds.isEmpty()) {
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ Component.text(
+ languageManager.getMessage("channel.status.noChannels"),
+ NamedTextColor.GRAY,
+ ),
+ ),
+ )
+ } else {
+ sender.sendMessage(
+ Component.text(
+ languageManager.getMessage(
+ "channel.status.channelList",
+ mapOf("count" to playerChannelIds.size.toString()),
+ ),
+ NamedTextColor.GOLD,
+ ),
+ )
+
+ playerChannelIds.forEach { channelId ->
+ val channel = channelManager.getChannel(channelId).getOrNull()
+ if (channel != null) {
+ val isOwner = channel.ownerId == sender.uniqueId
+ val isActive = channelId == activeChannelId
+
+ val hoverTextBuilder =
+ Component
+ .text()
+ .append(Component.text("ID: ", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+
+ if (isOwner) {
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.text("Role: ", NamedTextColor.GRAY))
+ .append(Component.text("Owner", NamedTextColor.GOLD))
+ }
+
+ if (!isActive) {
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.newline())
+ .append(Component.text(languageManager.getMessage("channel.status.clickToSwitch"), NamedTextColor.GREEN))
+ .append(Component.text(" ▶", NamedTextColor.YELLOW))
+ }
+
+ val hoverText = hoverTextBuilder.build()
+
+ var channelInfo =
+ Component
+ .text(" • ", NamedTextColor.GRAY)
+ .append(
+ Component
+ .text(channel.name, if (isActive) NamedTextColor.GREEN else NamedTextColor.AQUA)
+ .decorate(TextDecoration.ITALIC),
+ )
+
+ // Add owner indicator
+ if (isOwner) {
+ channelInfo = channelInfo.append(Component.text(" *", NamedTextColor.YELLOW))
+ }
+
+ channelInfo =
+ channelInfo
+ .append(Component.text(" (", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+ .append(Component.text(")", NamedTextColor.GRAY))
+ .hoverEvent(HoverEvent.showText(hoverText))
+
+ // Add click event if not active
+ if (!isActive) {
+ channelInfo = channelInfo.clickEvent(ClickEvent.runCommand("/lc channel switch ${channel.id}"))
+ }
+
+ sender.sendMessage(channelInfo)
+ }
+ }
+ }
+
+ 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/ChannelSwitchCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
new file mode 100644
index 0000000..13d44d2
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
@@ -0,0 +1,130 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+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
+
+@PlayerOnly
+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)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelSwitch::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("switch")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .suggests { ctx, builder ->
+ // Tab completion: suggest channels the player is a member of
+ val sender = ctx.source.executor
+ if (sender is org.bukkit.entity.Player) {
+ val playerChannels = membershipManager.getPlayerChannels(sender.uniqueId).getOrNull() ?: emptyList()
+ playerChannels.forEach { channelId ->
+ builder.suggest(channelId)
+ }
+ }
+ builder.buildFuture()
+ }.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val result = execute(context, channelId)
+ handleResult(context, result)
+ },
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = membershipManager.switchChannel(sender.uniqueId, channelId)
+ return result.fold(
+ 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),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ when (error) {
+ is ChannelNotFoundException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "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)),
+ ),
+ ),
+ )
+ }
+ is ChannelNotMemberException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.switch.notMember",
+ mapOf("channelId" to channelId),
+ ),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("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/chatmode/ChatModeToggleCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
new file mode 100644
index 0000000..cea1814
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
@@ -0,0 +1,71 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.chatmode
+
+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.chat.ChatModeManager
+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 net.kyori.adventure.text.Component
+import net.kyori.adventure.text.format.NamedTextColor
+
+@PlayerOnly
+class ChatModeToggleCommand(
+ plugin: LunaticChat,
+ private val chatModeManager: ChatModeManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChatModeToggle::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("toggle").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+ val newMode = chatModeManager.toggleChatMode(sender.uniqueId)
+
+ val modeKey =
+ when (newMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> "chatmode.mode.global"
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> "chatmode.mode.channel"
+ }
+
+ val modeColor =
+ when (newMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> NamedTextColor.GREEN
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> NamedTextColor.AQUA
+ }
+
+ sender.sendMessage(
+ Component
+ .text()
+ .append(MessageFormatter.formatSuccess(languageManager.getMessage("chatmode.toggle.success") + ": "))
+ .append(Component.text(languageManager.getMessage(modeKey), modeColor))
+ .build(),
+ )
+
+ return CommandResult.Success
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChatModeToggleCommand should use build() method instead of buildCommand()",
+ )
+}
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 26a1f06..076a7aa 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
@@ -19,11 +19,17 @@ sealed class SettingKey(
*/
data object Notice : SettingKey("notice")
+ /**
+ * Channel message notification setting
+ * Command: /lc setting chNotice <on|off>
+ */
+ data object ChNotice : SettingKey("chNotice")
+
companion object {
/**
* Returns all available setting keys.
*/
- fun values(): List<SettingKey> = listOf(Japanese, Notice)
+ fun values(): List<SettingKey> = listOf(Japanese, Notice, ChNotice)
/**
* Finds a setting key by its string representation.
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
new file mode 100644
index 0000000..8867e7c
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt
@@ -0,0 +1,53 @@
+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
index 90ff0ad..1564615 100644
--- 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
@@ -30,7 +30,7 @@ class DirectMessageNoticeSettingHandler(
val toggleText = languageManager.getToggleText(enable)
val message =
MessageFormatter.formatSuccess(
- languageManager.getMessage("directMessageNoticeToggle", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("directMessage.noticeToggle", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
@@ -44,7 +44,7 @@ class DirectMessageNoticeSettingHandler(
val toggleText = languageManager.getToggleText(settings.directMessageNotificationEnabled)
val message =
MessageFormatter.format(
- languageManager.getMessage("directMessageNoticeStatus", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("directMessage.noticeStatus", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
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
index 39cbc0d..2c682ba 100644
--- 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
@@ -30,7 +30,7 @@ class JapaneseConversionSettingHandler(
val toggleText = languageManager.getToggleText(enable)
val message =
MessageFormatter.formatSuccess(
- languageManager.getMessage("romajiConversionToggle", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("romajiConversion.toggle", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
@@ -44,7 +44,7 @@ class JapaneseConversionSettingHandler(
val toggleText = languageManager.getToggleText(settings.japaneseConversionEnabled)
val message =
MessageFormatter.format(
- languageManager.getMessage("romajiConversionStatus", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("romajiConversion.status", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt
index 16c8d75..e56c356 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt
@@ -31,6 +31,14 @@ object SoundCollector {
0.6f,
2.0f,
)
+
+ val LUNATIC_BELL_SOUND: Sound =
+ Sound.sound(
+ Key.key("block.note_block.bell"),
+ Sound.Source.PLAYER,
+ 0.7f,
+ 1.2f,
+ )
}
/**
@@ -43,6 +51,20 @@ fun Player.playDirectMessageNotification() {
/**
* Plays the message sent sound to the player.
*/
-fun Player.playDirectMessageSendNotification() {
+fun Player.playMessageSendNotification() {
playSound(SoundCollector.LUNATIC_POP_SOUND)
}
+
+/**
+ * Plays the channel join notification sound to the player.
+ */
+fun Player.playChannelJoinNotification() {
+ playSound(SoundCollector.LUNATIC_SOFT_SOUND)
+}
+
+/**
+ * Plays the channel message receive notification sound to the player.
+ */
+fun Player.playChannelReceiveNotification() {
+ playSound(SoundCollector.LUNATIC_BELL_SOUND)
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
index 7661089..9aa563a 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
@@ -1,5 +1,6 @@
package dev.m1sk9.lunaticChat.paper.config
+import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
import dev.m1sk9.lunaticChat.paper.config.key.JapaneseConversionFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
@@ -7,6 +8,8 @@ import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig
import dev.m1sk9.lunaticChat.paper.i18n.Language
import org.bukkit.configuration.file.FileConfiguration
+// FIXME: ConfigManager uses mutable static state which makes testing difficult
+// and creates hidden global dependencies. Consider refactoring to dependency injection.
object ConfigManager {
private var lunaticChatConfiguration: LunaticChatConfiguration? = null
@@ -18,7 +21,7 @@ object ConfigManager {
LunaticChatConfiguration(
features =
FeaturesConfig(
- quickRepliesEnabled =
+ quickReplies =
QuickRepliesFeatureConfig(
enabled =
configFile.getBoolean("features.quickReplies.enabled", true),
@@ -44,6 +47,10 @@ object ConfigManager {
),
apiRetryAttempts = configFile.getInt("features.japaneseConversion.api.retryAttempts", 2),
),
+ channelChat =
+ ChannelChatFeatureConfig(
+ enabled = configFile.getBoolean("features.channelChat.enabled", false),
+ ),
),
messageFormat =
MessageFormatConfig(
@@ -52,6 +59,11 @@ object ConfigManager {
"messageFormat.directMessageFormat",
"§7[§e{sender} §7>> §e{recipient}§7] §f{message}",
)!!,
+ channelMessageFormat =
+ configFile.getString(
+ "messageFormat.channelMessageFormat",
+ "§7[§b#{channel}§7] §e{sender}: §f{message}",
+ )!!,
),
debug = configFile.getBoolean("debug", false),
checkForUpdates = configFile.getBoolean("checkForUpdates", false),
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt
new file mode 100644
index 0000000..828bdb3
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.paper.config.key
+
+data class ChannelChatFeatureConfig(
+ val enabled: Boolean,
+)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
index 703ddcc..190e00c 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
@@ -1,6 +1,7 @@
package dev.m1sk9.lunaticChat.paper.config.key
data class FeaturesConfig(
- val quickRepliesEnabled: QuickRepliesFeatureConfig,
+ val quickReplies: QuickRepliesFeatureConfig,
val japaneseConversion: JapaneseConversionFeatureConfig,
+ val channelChat: ChannelChatFeatureConfig,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt
index 53d4399..2e623fa 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt
@@ -2,4 +2,5 @@ package dev.m1sk9.lunaticChat.paper.config.key
data class MessageFormatConfig(
val directMessageFormat: String = "§7[§e{sender} §7>> §e{recipient}§7] §f{message}",
+ val channelMessageFormat: String = "§7[§b#{channel}§7] §e{sender}: §f{message}",
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
index d2ba632..da79cfa 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
@@ -110,17 +110,20 @@ class ConversionCache(
private fun queueSaveToDisk() {
if (conversionSaveQueue.compareAndSet(false, true)) {
- Bukkit.getScheduler().runTaskAsynchronously(
+ Bukkit.getScheduler().runTaskLaterAsynchronously(
plugin,
Runnable {
- Thread.sleep(5000) // 5 seconds delay to batch multiple save requests
- conversionSaveQueue.set(true)
+ conversionSaveQueue.set(false)
saveToDisk()
},
+ 100L, // 5 seconds = 100 ticks
)
}
}
+ // FIXME: ConcurrentHashMap keys are unordered, so evicting "oldest" entries
+ // actually evicts random entries. Consider using LinkedHashMap with access-order
+ // or implement proper LRU cache with timestamp tracking.
private fun evictOldestEntry() {
val toRemove = conversionMemoryCache.size / 10
conversionMemoryCache.keys.take(toRemove).forEach {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
index 8d4070d..f8e8181 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
@@ -29,14 +29,29 @@ object EventListenerRegistry {
// Always register these listeners
pluginManager.registerEvents(SpyPermissionManager, plugin)
pluginManager.registerEvents(
- PlayerPresenceListener(plugin, services.languageManager, updateAvailable),
+ PlayerPresenceListener(
+ lunaticChat = plugin,
+ languageManager = services.languageManager,
+ updateCheckerFlag = updateAvailable,
+ playerSettingsManager = services.playerSettingsManager,
+ chatModeManager = services.chatModeManager,
+ channelManager = services.channelManager,
+ ),
plugin,
)
- // Conditionally register Japanese conversion listener
- if (services.romajiConverter != null) {
+ // Conditionally register chat listener when all required components are available
+ if (services.chatModeManager != null &&
+ services.channelMessageHandler != null &&
+ services.romajiConverter != null
+ ) {
pluginManager.registerEvents(
- PlayerChatListener(services.romajiConverter, services.playerSettingsManager),
+ PlayerChatListener(
+ chatModeManager = services.chatModeManager,
+ channelMessageHandler = services.channelMessageHandler,
+ romajiConverter = services.romajiConverter,
+ settingsManager = services.playerSettingsManager,
+ ),
plugin,
)
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
index 17df427..47c8cd4 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
@@ -1,5 +1,8 @@
package dev.m1sk9.lunaticChat.paper.listener
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import io.papermc.paper.event.player.AsyncChatEvent
@@ -10,30 +13,66 @@ import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class PlayerChatListener(
+ private val chatModeManager: ChatModeManager,
+ private val channelMessageHandler: ChannelMessageHandler,
private val romajiConverter: RomanjiConverter,
private val settingsManager: PlayerSettingsManager,
) : Listener {
private val plainTextSerializer = PlainTextComponentSerializer.plainText()
@EventHandler(ignoreCancelled = true)
- fun onChat(event: AsyncChatEvent) =
- runBlocking {
- val player = event.player
- val settings = settingsManager.getSettings(player.uniqueId)
- val message = event.message()
- val originalMessage = plainTextSerializer.serialize(message)
-
- val displayMessage =
- settings
- .takeIf {
- it.japaneseConversionEnabled
- }?.runCatching {
+ fun onChat(event: AsyncChatEvent) {
+ val player = event.player
+ val settings = settingsManager.getSettings(player.uniqueId)
+
+ val originalMessage = plainTextSerializer.serialize(event.message())
+
+ // Handle chat mode switching with '!' prefix
+ val hasPrefix = originalMessage.startsWith('!')
+ val messageWithoutPrefix =
+ if (hasPrefix) {
+ originalMessage.removePrefix("!").trim()
+ } else {
+ originalMessage
+ }
+
+ if (hasPrefix && messageWithoutPrefix.isEmpty()) {
+ event.isCancelled = true
+ return
+ }
+
+ val effectiveMode =
+ if (hasPrefix) {
+ val currentMode = chatModeManager.getChatMode(player.uniqueId)
+ currentMode.toggle()
+ } else {
+ chatModeManager.getChatMode(player.uniqueId)
+ }
+
+ // Handle romaji conversion if enabled (requires blocking for HTTP call)
+ val displayMessage =
+ if (settings.japaneseConversionEnabled) {
+ runCatching {
+ runBlocking {
romajiConverter
- .convert(originalMessage)
- ?.let { "$originalMessage §e($it)" }
- ?: originalMessage
- }?.getOrNull() ?: originalMessage
+ .convert(messageWithoutPrefix)
+ ?.let { "$messageWithoutPrefix §e($it)" }
+ ?: messageWithoutPrefix
+ }
+ }.getOrNull() ?: messageWithoutPrefix
+ } else {
+ messageWithoutPrefix
+ }
- event.message(Component.text(displayMessage))
+ // Route message based on chat mode
+ when (effectiveMode) {
+ ChatMode.GLOBAL -> {
+ event.message(Component.text(displayMessage))
+ }
+ ChatMode.CHANNEL -> {
+ event.isCancelled = true
+ channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix)
+ }
}
+ }
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
index 1b53453..fe4239d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
@@ -1,9 +1,14 @@
package dev.m1sk9.lunaticChat.paper.listener
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
import dev.m1sk9.lunaticChat.paper.common.hasAnyPermission
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.event.ClickEvent
import org.bukkit.event.EventHandler
@@ -16,24 +21,72 @@ class PlayerPresenceListener(
private val lunaticChat: LunaticChat,
private val languageManager: LanguageManager,
private val updateCheckerFlag: AtomicBoolean,
+ private val playerSettingsManager: PlayerSettingsManager,
+ private val chatModeManager: ChatModeManager? = null,
+ private val channelManager: ChannelManager? = null,
) : Listener {
@EventHandler(ignoreCancelled = true)
fun onJoin(event: PlayerJoinEvent) {
val player = event.player
- if (!updateCheckerFlag.get() || !player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) return
-
- player.sendMessage {
- Component
- .text(
- languageManager.getMessage("newUpdateAvailable"),
- ).clickEvent(
- ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest"),
+
+ // Send update notification if available
+ if (updateCheckerFlag.get() && player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) {
+ player.sendMessage {
+ Component
+ .text(
+ languageManager.getMessage("general.newUpdateAvailable"),
+ ).clickEvent(
+ ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest"),
+ )
+ }
+ }
+
+ // Send chat mode notification
+ chatModeManager?.let { manager ->
+ val chatMode = manager.getChatMode(player.uniqueId)
+ val modeKey =
+ when (chatMode) {
+ ChatMode.GLOBAL -> "chatmode.mode.global"
+ ChatMode.CHANNEL -> "chatmode.mode.channel"
+ }
+ val modeText = languageManager.getMessage(modeKey)
+ val notification =
+ languageManager.getMessage(
+ "chatmode.notification.login",
+ mapOf("mode" to modeText),
)
+ player.sendMessage(MessageFormatter.format(notification))
+ }
+
+ // Send channel notification if in a channel
+ channelManager?.let { manager ->
+ val context = manager.getPlayerChannelContext(player.uniqueId)
+ context?.let {
+ val notification =
+ languageManager.getMessage(
+ "channel.notification.login",
+ mapOf("channelName" to it.channel.name),
+ )
+ player.sendMessage(Component.text(notification))
+ }
}
}
@EventHandler(ignoreCancelled = true)
fun onQuit(event: PlayerQuitEvent) {
- lunaticChat.directMessageHandler.clearPlayer(event.player)
+ val player = event.player
+ val playerId = player.uniqueId
+
+ // 1. Clear direct message references
+ lunaticChat.directMessageHandler.clearPlayer(player)
+
+ // 2. Clear active channel for this player
+ channelManager?.setPlayerChannel(playerId, null)
+
+ // 3. Trigger async save of chat mode data
+ chatModeManager?.saveToDisk()
+
+ // 4. Trigger async save of player settings
+ playerSettingsManager.saveToDisk()
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
index a30618b..f0927d8 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
@@ -19,6 +19,7 @@ class PlayerSettingsManager(
) {
private val japaneseConversionCache = ConcurrentHashMap<UUID, Boolean>()
private val directMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>()
+ private val channelMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>()
private lateinit var settingsData: PlayerSettingsData
/**
@@ -29,6 +30,7 @@ class PlayerSettingsManager(
settingsData = storage.loadFromDisk()
japaneseConversionCache.putAll(settingsData.japaneseConversion)
directMessageNotificationCache.putAll(settingsData.directMessageNotification)
+ channelMessageNotificationCache.putAll(settingsData.channelMessageNotification)
logger.info("Loaded settings for ${japaneseConversionCache.size} players")
}
@@ -42,10 +44,12 @@ class PlayerSettingsManager(
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,
)
}
@@ -57,11 +61,13 @@ class PlayerSettingsManager(
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)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
index 754c82b..0aa8eef 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
@@ -87,13 +87,13 @@ class YamlPlayerSettingsStorage(
*/
fun queueAsyncSave(data: PlayerSettingsData) {
if (saveFlag.compareAndSet(false, true)) {
- Bukkit.getScheduler().runTaskAsynchronously(
+ Bukkit.getScheduler().runTaskLaterAsynchronously(
plugin,
Runnable {
- Thread.sleep(5000) // 5 seconds delay to batch multiple save requests
saveFlag.set(false)
saveToDisk(data)
},
+ 100L, // 5 seconds = 100 ticks
)
}
}
diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml
index d4ca25b..839e0e3 100644
--- a/platform-paper/src/main/resources/config.yml
+++ b/platform-paper/src/main/resources/config.yml
@@ -48,6 +48,9 @@ features:
timeout: 3000
# Specify the number of retry attempts for failed API requests to the Romanization conversion service.
retryAttempts: 2
+ channelChat:
+ # If enabled, channel-based chat functionality will be activated.
+ enabled: false
# ----------------------------------------------
# --------- Message Format Settings --------
@@ -59,9 +62,12 @@ features:
# {sender} - The name of the message sender
# {recipient} - The name of the message recipient
# {message} - The content of the message
+# {channel} - The name of the chat channel (only for channel chat)
# ----------------------------------------------
messageFormat:
# Configure the format for direct messages sent via /tell or /msg
directMessageFormat: "§7[§e{sender} §7>> §e{recipient}§7] §f{message}"
+ # Configure the format for messages sent in channel chat
+ channelMessageFormat: "§7[§b#{channel}§7] §e{sender}: §f{message}"
diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml
index d61d03f..925d7e7 100644
--- a/platform-paper/src/main/resources/languages/en.yml
+++ b/platform-paper/src/main/resources/languages/en.yml
@@ -14,23 +14,99 @@
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"
-directMessageNoticeStatus: "Direct message notifications are currently {toggle}"
-directMessageNoticeToggle: "Direct message notifications have been set to {toggle}"
-playerOnlyCommand: "This command can only be executed by players."
-replyTargetNotFound: "Reply target player not found."
-romajiConversionToggle: "Romaji-to-kana conversion has been set to {toggle}"
-romajiConversionStatus: "Romaji-to-kana conversion is currently {toggle}"
-settingsAvailableValues: "Available settings value: {values}"
-statusHover: "LunaticChat was born as the successor to LunaChat. With respect to its original creator, uccchyocean."
-statusRunningVersion: "Running LunaticChat v{version}"
-tellTargetOffline: "Player '{target}' is currently offline."
-tellYourself: "You cannot send a message to yourself."
-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."
+directMessage:
+ noticeStatus: "Direct message notifications are currently {toggle}"
+ noticeToggle: "Direct message notifications have been set to {toggle}"
+ targetOffline: "Player '{target}' is currently offline."
+ yourself: "You cannot send a message to yourself."
+ replyTargetNotFound: "Reply target player not found."
+
+romajiConversion:
+ toggle: "Romaji-to-kana conversion has been set to {toggle}"
+ status: "Romaji-to-kana conversion is currently {toggle}"
+
+channelMessage:
+ noticeToggle: "Channel message notifications have been set to {toggle}"
+ noticeStatus: "Channel message notifications are currently {toggle}"
+
+status:
+ hover: "LunaticChat was born as the successor to LunaChat. With respect to its original creator, uccchyocean."
+ runningVersion: "Running LunaticChat v{version}"
+
+settings:
+ availableValues: "Available settings value: {values}"
+
+channel:
+ help:
+ header: "=== Channel Commands ==="
+ create: "/lc channel create <channelId> <name> [description] [isPrivate] - Create a new channel"
+ list: "/lc channel list - List all public channels"
+ join: "/lc channel join <channelId> - Join a channel"
+ leave: "/lc channel leave - Leave the active channel"
+ switch: "/lc channel switch <channelId> - Switch to a channel you're already a member of"
+ status: "/lc channel status - Show your channel status and memberships"
+ delete: "/lc channel delete <channelId> - Delete a channel"
+ create:
+ success: "Created channel '{name}' (ID: {id})"
+ alreadyExists: "Channel ID '{id}' is already in use"
+ invalidId: "Invalid channel ID '{id}'. Must be 3-30 characters using only letters, numbers, underscores, and hyphens"
+ list:
+ header: "=== Public Channels ({count}) ==="
+ empty: "No public channels available"
+ error: "Failed to retrieve channel list"
+ clickToJoin: "Click to join"
+ delete:
+ success: "Deleted channel '{id}'"
+ notFound: "Channel '{id}' not found"
+ noPermission: "You don't have permission to delete this channel. Only the channel owner can delete it"
+ error: "Failed to delete channel"
+ join:
+ success: "Joined channel '{channelName}' (ID: {channelId})"
+ alreadyActive: "Channel '{channelName}' is already your active channel"
+ alreadyMember: "You are already a member of channel '{channelName}'"
+ notFound: "Channel '{channelId}' not found"
+ privateChannel: "This is a private channel and requires an invitation"
+ error: "Failed to join channel"
+ leave:
+ success: "Left channel '{channelName}'"
+ noActiveChannel: "You don't have an active channel"
+ error: "Failed to leave channel"
+ switch:
+ success: "Switched to channel '{channelName}' (ID: {channelId})"
+ alreadyActive: "Channel '{channelName}' is already your active channel"
+ notFound: "Channel '{channelId}' not found"
+ notMember: "You are not a member of channel '{channelId}'"
+ error: "Failed to switch channel"
+ status:
+ header: "=== Your Channel Status ==="
+ activeChannel: "Active Channel"
+ noActiveChannel: "No active channel"
+ channelList: "Your Channels ({count})"
+ noChannels: "You are not a member of any channels"
+ clickToSwitch: "Click to switch"
+ error: "Failed to retrieve channel status"
+ notification:
+ login: "You are in channel '{channelName}'"
+
+chatmode:
+ current: "Current chat mode"
+ mode:
+ global: "GLOBAL"
+ channel: "CHANNEL"
+ toggle:
+ success: "Chat mode switched to"
+ notification:
+ login: "Current chat mode: {mode}"
+
+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."
toggle:
off: "Disabled"
diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml
index 5241cc8..17b0bf6 100644
--- a/platform-paper/src/main/resources/languages/ja.yml
+++ b/platform-paper/src/main/resources/languages/ja.yml
@@ -16,21 +16,97 @@ commandDescription:
reply: "最後にダイレクトメッセージを送信/受信したプレイヤーに返信します"
jp: "かな・ローマ字変換機能のオン/オフを切り替えます"
notice: "ダイレクトメッセージ通知のオン/オフを切り替えます"
+ chNotice: "チャンネルメッセージ通知のオン/オフを切り替えます"
lc: "LunaticChat のメインコマンド"
-directMessageNoticeToggle: "ダイレクトメッセージ通知を{toggle}にしました"
-directMessageNoticeStatus: "現在ダイレクトメッセージ通知は{toggle}です"
-playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます"
-replyTargetNotFound: "返信対象のプレイヤーが見つかりません"
-romajiConversionToggle: "かな・ローマ字変換機能を{toggle}にしました"
-romajiConversionStatus: "現在かな・ローマ字変換機能は{toggle}です"
-settingsAvailableValues: "使用可能な設定値: {values}"
-statusHover: "LunaticChat は LunaChatの後継として生まれました。\nオリジナルの作者であるucchyoceanに敬意を込めて。"
-statusRunningVersion: "LunaticChat v{version} が使用されています"
-tellTargetOffline: "プレイヤー '{target}' は現在オフラインです"
-tellYourself: "自分自身にメッセージを送信することはできません"
-newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます"
-noPermission: "このコマンドを実行する権限がありません"
+directMessage:
+ noticeToggle: "ダイレクトメッセージ通知を{toggle}にしました"
+ noticeStatus: "現在ダイレクトメッセージ通知は{toggle}です"
+ targetOffline: "プレイヤー '{target}' は現在オフラインです"
+ yourself: "自分自身にメッセージを送信することはできません"
+ replyTargetNotFound: "返信対象のプレイヤーが見つかりません"
+
+romajiConversion:
+ toggle: "かな・ローマ字変換機能を{toggle}にしました"
+ status: "現在かな・ローマ字変換機能は{toggle}です"
+
+channelMessage:
+ noticeToggle: "チャンネルメッセージ通知を{toggle}にしました"
+ noticeStatus: "現在チャンネルメッセージ通知は{toggle}です"
+
+status:
+ hover: "LunaticChat は LunaChatの後継として生まれました。\nオリジナルの作者であるucchyoceanに敬意を込めて。"
+ runningVersion: "LunaticChat v{version} が使用されています"
+
+settings:
+ availableValues: "使用可能な設定値: {values}"
+
+channel:
+ help:
+ header: "=== チャンネルコマンド ==="
+ create: "/lc channel create <チャンネルID> <名前> [説明] [プライベート] - チャンネルを作成します"
+ list: "/lc channel list - 公開チャンネル一覧を表示します"
+ join: "/lc channel join <チャンネルID> - チャンネルに参加します"
+ leave: "/lc channel leave - アクティブチャンネルから退出します"
+ switch: "/lc channel switch <チャンネルID> - 既に参加しているチャンネルに切り替えます"
+ status: "/lc channel status - チャンネルのステータスとメンバーシップを表示します"
+ delete: "/lc channel delete <チャンネルID> - チャンネルを削除します"
+ create:
+ success: "チャンネル '{name}' (ID: {id}) を作成しました"
+ alreadyExists: "チャンネルID '{id}' は既に使用されています"
+ invalidId: "無効なチャンネルID '{id}' です。3-30文字の英数字、アンダースコア、ハイフンのみ使用できます"
+ list:
+ header: "=== 公開チャンネル ({count}件) ==="
+ empty: "公開チャンネルはありません"
+ error: "チャンネル一覧の取得に失敗しました"
+ clickToJoin: "クリックして参加"
+ delete:
+ success: "チャンネル '{id}' を削除しました"
+ notFound: "チャンネル '{id}' が見つかりません"
+ noPermission: "このチャンネルを削除する権限がありません。チャンネルのオーナーのみが削除できます"
+ error: "チャンネルの削除に失敗しました"
+ join:
+ success: "チャンネル '{channelName}' (ID: {channelId}) に参加しました"
+ alreadyActive: "チャンネル '{channelName}' は既にアクティブです"
+ alreadyMember: "既にチャンネル '{channelName}' のメンバーです"
+ notFound: "チャンネル '{channelId}' が見つかりません"
+ privateChannel: "プライベートチャンネルには招待が必要です"
+ error: "チャンネルへの参加に失敗しました"
+ leave:
+ success: "チャンネル '{channelName}' から退出しました"
+ noActiveChannel: "アクティブなチャンネルがありません"
+ error: "チャンネルからの退出に失敗しました"
+ switch:
+ success: "チャンネル '{channelName}' (ID: {channelId}) に切り替えました"
+ alreadyActive: "チャンネル '{channelName}' は既にアクティブです"
+ notFound: "チャンネル '{channelId}' が見つかりません"
+ notMember: "チャンネル '{channelId}' のメンバーではありません"
+ error: "チャンネルの切り替えに失敗しました"
+ status:
+ header: "=== あなたのチャンネルステータス ==="
+ activeChannel: "アクティブチャンネル"
+ noActiveChannel: "アクティブなチャンネルがありません"
+ channelList: "参加中のチャンネル ({count}件)"
+ noChannels: "どのチャンネルにも参加していません"
+ clickToSwitch: "クリックして切り替え"
+ error: "チャンネルステータスの取得に失敗しました"
+ notification:
+ login: "チャンネル '{channelName}' に入室中です"
+
+chatmode:
+ current: "現在のチャットモード"
+ mode:
+ global: "グローバル"
+ channel: "チャンネル"
+ toggle:
+ success: "チャットモードを切り替えました"
+ notification:
+ login: "現在のチャットモード: {mode}"
+
+general:
+ playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます"
+ newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます"
+ noPermission: "このコマンドを実行する権限がありません"
toggle:
on: "有効"
diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml
index 3f991a9..b3a9d8f 100644
--- a/platform-paper/src/main/resources/paper-plugin.yml
+++ b/platform-paper/src/main/resources/paper-plugin.yml
@@ -19,6 +19,26 @@ permissions:
default: true
lunaticchat.command.lc.status:
default: true
+ lunaticchat.command.lc.channel:
+ default: true
+ lunaticchat.command.lc.channel.create:
+ default: true
+ lunaticchat.command.lc.channel.list:
+ default: true
+ lunaticchat.command.lc.channel.join:
+ default: true
+ lunaticchat.command.lc.channel.leave:
+ default: true
+ lunaticchat.command.lc.channel.switch:
+ default: true
+ lunaticchat.command.lc.channel.status:
+ default: true
+ lunaticchat.command.lc.channel.delete:
+ default: true
+ lunaticchat.command.lc.chatmode:
+ default: true
+ lunaticchat.command.lc.chatmode.toggle:
+ default: true
lunaticchat.spy:
default: op