diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-25 17:28:04 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-26 00:35:42 +0900 |
| commit | 1dc724dbb96a0d1fdc36c5105538e67efa5b0b87 (patch) | |
| tree | ff0481301e10fb8419fb7c6d2c2ab5689a9fe225 /platform-paper/src | |
| parent | d35eff7d10d6bebd5231e7a5d59a8a13f241e1ee (diff) | |
| download | LunaticChat-1dc724dbb96a0d1fdc36c5105538e67efa5b0b87.tar.gz LunaticChat-1dc724dbb96a0d1fdc36c5105538e67efa5b0b87.tar.bz2 LunaticChat-1dc724dbb96a0d1fdc36c5105538e67efa5b0b87.zip | |
feat: Add channel command
Diffstat (limited to 'platform-paper/src')
17 files changed, 1543 insertions, 31 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 dd6d618..046d61f 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,5 +1,7 @@ package dev.m1sk9.lunaticChat.paper +import dev.m1sk9.lunaticChat.paper.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.command.impl.ReplyCommand @@ -27,6 +29,8 @@ 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 // Private services private lateinit var services: ServiceContainer @@ -60,6 +64,8 @@ class LunaticChat : // Set public API properties (for command access) directMessageHandler = services.directMessageHandler languageManager = services.languageManager + channelManager = services.channelManager + channelMembershipManager = services.channelMembershipManager // Schedule periodic tasks serviceInitializer.schedulePeriodicTasks() 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..636ccfb 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,5 +1,7 @@ package dev.m1sk9.lunaticChat.paper +import dev.m1sk9.lunaticChat.paper.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager @@ -15,10 +17,14 @@ 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) */ data class ServiceContainer( val languageManager: LanguageManager, val playerSettingsManager: PlayerSettingsManager, val directMessageHandler: DirectMessageHandler, val romajiConverter: RomanjiConverter? = null, + val channelManager: ChannelManager? = null, + val channelMembershipManager: ChannelMembershipManager? = 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 fa5181b..17d99f6 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,6 +1,8 @@ package dev.m1sk9.lunaticChat.paper import dev.m1sk9.lunaticChat.engine.converter.GoogleIMEClient +import dev.m1sk9.lunaticChat.paper.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.channel.ChannelStorage import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration @@ -27,6 +29,8 @@ class ServiceInitializer( private val logger: Logger, ) { private var conversionCache: ConversionCache? = null + private var channelManager: ChannelManager? = null + private var channelMembershipManager: ChannelMembershipManager? = null /** * Initializes all services in dependency order. @@ -62,10 +66,13 @@ class ServiceInitializer( null } - // 4. Initialize channel storage - if (configuration.features.channelChat.enabled) { - initializeChannelStorage() - } + // 4. Initialize channel manager and membership manager + val (channelManager, channelMembershipManager) = + if (configuration.features.channelChat.enabled) { + initializeChannelManager() + } else { + Pair(null, null) + } // 5. Initialize handlers val directMessageHandler = @@ -79,6 +86,8 @@ class ServiceInitializer( playerSettingsManager = playerSettingsManager, directMessageHandler = directMessageHandler, romajiConverter = romajiConverter, + channelManager = channelManager, + channelMembershipManager = channelMembershipManager, ) } @@ -143,9 +152,9 @@ class ServiceInitializer( } /** - * Initializes channel storage by loading existing data or creating new storage. + * Initializes channel manager and membership manager with storage. */ - private fun initializeChannelStorage() { + private fun initializeChannelManager(): Pair<ChannelManager, ChannelMembershipManager> { val channelsFile = plugin.dataFolder.resolve("channels.json").toPath() val storage = ChannelStorage( @@ -154,10 +163,23 @@ class ServiceInitializer( logger = logger, ) - val channelData = storage.loadFromDisk() - storage.saveToDisk(channelData) + val manager = + ChannelManager( + storage = storage, + logger = logger, + ) + manager.initialize() + channelManager = manager + + val membershipManager = + ChannelMembershipManager( + channelManager = manager, + logger = logger, + ) + channelMembershipManager = membershipManager - logger.info("Channels storage loaded successfully.") + logger.info("Channel manager and membership manager initialized successfully.") + return Pair(manager, membershipManager) } /** @@ -183,5 +205,6 @@ class ServiceInitializer( fun shutdown(services: ServiceContainer) { services.playerSettingsManager.saveToDisk() conversionCache?.saveToDisk() + services.channelManager?.saveToDisk() } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelManager.kt index a332e02..d18c2a5 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelManager.kt @@ -6,8 +6,8 @@ import dev.m1sk9.lunaticChat.engine.channel.modal.ChannelMember import dev.m1sk9.lunaticChat.engine.channel.modal.ChannelRole import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException -import io.ktor.util.collections.ConcurrentMap import java.util.UUID +import java.util.concurrent.ConcurrentHashMap import java.util.logging.Logger import kotlin.collections.forEach @@ -15,8 +15,9 @@ class ChannelManager( private val storage: ChannelStorage, private val logger: Logger, ) { - private val channelsCache = ConcurrentMap<String, Channel>() - private val membersCache = ConcurrentMap<String, MutableList<ChannelMember>>() + private val channelsCache = ConcurrentHashMap<String, Channel>() + private val membersCache = ConcurrentHashMap<String, MutableList<ChannelMember>>() + private val activeChannels = ConcurrentHashMap<UUID, String>() /** * Initializes the ChannelManager by loading data from storage. @@ -27,7 +28,15 @@ class ChannelManager( data.members.forEach { (channelId, members) -> membersCache[channelId] = members.toMutableList() } - logger.info("ChannelManager initialized with ${channelsCache.size} channels.") + 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.") } /** @@ -52,6 +61,9 @@ class ChannelManager( ) membersCache[channel.id] = mutableListOf(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) @@ -77,6 +89,9 @@ class ChannelManager( 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) @@ -127,15 +142,77 @@ class ChannelManager( * @throws ChannelNotFoundException if the channel does not exist. */ fun getChannelMembers(channelId: String): Result<List<ChannelMember>> { - val channel = - channelsCache[channelId] - ?: return Result.failure(ChannelNotFoundException(channelId)) + 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) { + mutableListOf() + } + 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() { @@ -143,6 +220,7 @@ class ChannelManager( 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.") @@ -157,7 +235,34 @@ class ChannelManager( 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] + + /** + * 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/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelMembershipManager.kt index 29c6bd9..41c96c4 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelMembershipManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelMembershipManager.kt @@ -1,7 +1,11 @@ package dev.m1sk9.lunaticChat.paper.channel import dev.m1sk9.lunaticChat.engine.channel.modal.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 @@ -68,4 +72,153 @@ class ChannelMembershipManager( }, 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/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..dfa7e9c --- /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.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.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/LunaticChatCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt index 2d634be..c89f579 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,37 @@ 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( - plugin, - languageManager, - ).buildWithPermissionCheck(), - ) + 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(), + ) + } + } + + return command + } } 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..14254c8 --- /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.channel.modal.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.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..4d547ea --- /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.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..4f40ad2 --- /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.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.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..7b09bde --- /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.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.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..e2cdbb5 --- /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.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..7b5cabd --- /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.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.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..ca62baf --- /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.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.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/common/SoundCollector.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt index 16c8d75..b0adbf5 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 @@ -46,3 +46,10 @@ fun Player.playDirectMessageNotification() { fun Player.playDirectMessageSendNotification() { playSound(SoundCollector.LUNATIC_POP_SOUND) } + +/** + * Plays the channel join notification sound to the player. + */ +fun Player.playChannelJoinNotification() { + playSound(SoundCollector.LUNATIC_SOFT_SOUND) +} diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml index fde5f52..6834428 100644 --- a/platform-paper/src/main/resources/languages/en.yml +++ b/platform-paper/src/main/resources/languages/en.yml @@ -36,6 +36,56 @@ status: 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" + 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." diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml index fbde9a7..77be961 100644 --- a/platform-paper/src/main/resources/languages/ja.yml +++ b/platform-paper/src/main/resources/languages/ja.yml @@ -36,6 +36,56 @@ status: 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: "チャンネルステータスの取得に失敗しました" + general: playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます" newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます" |
