diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-26 02:58:23 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-26 02:58:23 +0900 |
| commit | 18a4bbc4ba1a6766af920fb0849215f058ff260e (patch) | |
| tree | c570633f29f2398cb263e2eb90a7d56f0ea41e78 /platform-paper | |
| parent | 0fb8f4c92ad5dd7d20778de862b4d12f67f71d5e (diff) | |
| parent | c7714add1934baf55a5e6268b1a70780eaa84fea (diff) | |
| download | LunaticChat-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')
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 |
