diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-26 03:52:29 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-26 03:52:29 +0900 |
| commit | 817eb7bc479fa55bda106aa1f6c5bd8dc9fbbe4c (patch) | |
| tree | dde37372880748ace9e2f5b9de762214d1d04b3f /platform-paper | |
| parent | 18a4bbc4ba1a6766af920fb0849215f058ff260e (diff) | |
| download | LunaticChat-817eb7bc479fa55bda106aa1f6c5bd8dc9fbbe4c.tar.gz LunaticChat-817eb7bc479fa55bda106aa1f6c5bd8dc9fbbe4c.tar.bz2 LunaticChat-817eb7bc479fa55bda106aa1f6c5bd8dc9fbbe4c.zip | |
feat: Add channel-chat configuration
Diffstat (limited to 'platform-paper')
18 files changed, 486 insertions, 32 deletions
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 35f70d8..8135fe0 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 @@ -84,7 +84,7 @@ class ServiceInitializer( // 4. Initialize channel manager, membership manager, chat mode manager, and channel message handler val channelComponents = if (configuration.features.channelChat.enabled) { - initializeChannelManager(playerSettingsManager) + initializeChannelManager(playerSettingsManager, romajiConverter) } else { null } @@ -175,7 +175,10 @@ class ServiceInitializer( /** * Initializes channel manager, membership manager, chat mode manager, and channel message handler with storage. */ - private fun initializeChannelManager(settingsManager: PlayerSettingsManager): ChannelComponents { + private fun initializeChannelManager( + settingsManager: PlayerSettingsManager, + romajiConverter: RomanjiConverter?, + ): ChannelComponents { val channelsFile = plugin.dataFolder.resolve("channels.json").toPath() val storage = ChannelStorage( @@ -188,6 +191,7 @@ class ServiceInitializer( ChannelManager( storage = storage, logger = logger, + config = configuration.features.channelChat, ) manager.initialize() channelManager = manager @@ -196,6 +200,7 @@ class ServiceInitializer( ChannelMembershipManager( channelManager = manager, logger = logger, + config = configuration.features.channelChat, ) channelMembershipManager = membershipManager @@ -218,6 +223,7 @@ class ServiceInitializer( ChannelMessageHandler( settingsManager = settingsManager, channelManager = manager, + romanjiConverter = romajiConverter, logger = io.ktor.util.logging .KtorSimpleLogger("ChannelMessageHandler"), diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt index fc8d6da..679d300 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt @@ -5,8 +5,11 @@ 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.ChannelLimitExceededException +import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException +import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @@ -16,6 +19,7 @@ import kotlin.collections.forEach class ChannelManager( private val storage: ChannelStorage, private val logger: Logger, + private val config: ChannelChatFeatureConfig, ) { private val channelsCache = ConcurrentHashMap<String, Channel>() private val membersCache = ConcurrentHashMap<String, CopyOnWriteArrayList<ChannelMember>>() @@ -47,12 +51,18 @@ class ChannelManager( * @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. + * @throws ChannelLimitExceededException if the server has reached the maximum channel limit. */ fun createChannel(channel: Channel): Result<Channel> { if (channelsCache.containsKey(channel.id)) { return Result.failure(ChannelNotFoundException(channel.id)) } + // Check if max channels limit is reached (0 means unlimited) + if (config.maxChannelsPerServer > 0 && channelsCache.size >= config.maxChannelsPerServer) { + return Result.failure(ChannelLimitExceededException(config.maxChannelsPerServer)) + } + channelsCache[channel.id] = channel val ownerMember = @@ -159,6 +169,7 @@ class ChannelManager( * @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. + * @throws ChannelMemberLimitExceededException if the channel has reached the maximum member limit. */ fun addMember( channelId: String, @@ -172,6 +183,11 @@ class ChannelManager( membersCache.getOrPut(channelId) { CopyOnWriteArrayList() } + + // Check if max members limit is reached (0 means unlimited) + if (config.maxMembersPerChannel > 0 && members.size >= config.maxMembersPerChannel) { + return Result.failure(ChannelMemberLimitExceededException(channelId, config.maxMembersPerChannel)) + } val newMember = ChannelMember( channelId = channelId, diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt index 6d6ad8d..3c45eb4 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt @@ -6,12 +6,15 @@ 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 dev.m1sk9.lunaticChat.engine.exception.PlayerChannelLimitExceededException +import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig import java.util.UUID import java.util.logging.Logger class ChannelMembershipManager( private val channelManager: ChannelManager, private val logger: Logger, + private val config: ChannelChatFeatureConfig, ) { /** * Checks if a player is a member of a channel. @@ -83,6 +86,7 @@ class ChannelMembershipManager( * @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 PlayerChannelLimitExceededException if the player has reached the maximum channel membership limit. * @throws ChannelRuntimeException for other runtime errors. */ fun joinChannel( @@ -120,6 +124,23 @@ class ChannelMembershipManager( ) } + // Check if player has reached max membership limit (0 means unlimited) + if (config.maxMembershipPerPlayer > 0) { + val playerChannelCount = + getPlayerChannels(playerId) + .getOrElse { + return Result.failure( + ChannelRuntimeException("Failed to get player channels for $playerId", it), + ) + }.size + + if (playerChannelCount >= config.maxMembershipPerPlayer) { + return Result.failure( + PlayerChannelLimitExceededException(playerId, config.maxMembershipPerPlayer), + ) + } + } + // Add as member channelManager.addMember(channelId, playerId, ChannelRole.MEMBER).getOrElse { return Result.failure(it) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt index 70756d9..45e9d6d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt @@ -5,6 +5,7 @@ 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.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import io.ktor.util.logging.Logger import net.kyori.adventure.text.Component @@ -14,6 +15,7 @@ import org.bukkit.entity.Player class ChannelMessageHandler( private val settingsManager: PlayerSettingsManager?, private val channelManager: ChannelManager, + private val romanjiConverter: RomanjiConverter?, private val logger: Logger, ) { private var lunaticChatConfiguration = ConfigManager.getConfiguration() @@ -26,21 +28,37 @@ class ChannelMessageHandler( 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() + val senderSettings = settingsManager?.getSettings(playerId) + + // Handle romaji conversion if enabled (requires blocking for HTTP call) + val displayMessage = + if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) { + runCatching { + kotlinx.coroutines.runBlocking { + romanjiConverter + ?.convert(message) + ?.let { "$message §e($it)" } + ?: message + } + }.getOrNull() ?: message + } else { + message } + + val formattedMessage = formatChannelMessage(player.name, context.channel.name, displayMessage) + val spyMessage = formatChannelMessage(player.name, context.channel.name, message) + + // Play notification sound to sender if enabled + if (senderSettings?.channelMessageNotificationEnabled == true) { + player.playMessageSendNotification() } SpyPermissionManager .getDirectMessageSpyPlayers() .values .filter { it.isOnline && it.uniqueId != playerId } - .forEach { it.sendMessage(formattedMessage) } + .forEach { it.sendMessage(spyMessage) } context.members.forEach { member -> Bukkit.getPlayer(member.playerId)?.let { memberPlayer -> if (memberPlayer.isOnline) { diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt index 3e47c7e..90d8f31 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt @@ -12,6 +12,7 @@ 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.ChannelInfoCommand 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 @@ -82,6 +83,12 @@ class ChannelCommand( languageManager, ).buildWithPermissionCheck(), ).then( + ChannelInfoCommand( + plugin, + channelManager, + languageManager, + ).buildWithPermissionCheck(), + ).then( ChannelDeleteCommand( plugin, channelManager, @@ -168,6 +175,15 @@ class ChannelCommand( .text(" ") .append( MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.info"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( languageManager.getMessage("channel.help.delete"), ), ), 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 index 73a50e5..d0357f6 100644 --- 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 @@ -11,6 +11,7 @@ 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 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 @@ -69,9 +70,10 @@ class ChatModeCommand( } sender.sendMessage( - Component - .text(languageManager.getMessage("chatmode.current") + ": ", NamedTextColor.GRAY) - .append(Component.text(languageManager.getMessage(modeKey), modeColor)), + MessageFormatter + .format( + languageManager.getMessage("chatmode.current") + ": ", + ).append(Component.text(languageManager.getMessage(modeKey), modeColor)), ) return CommandResult.Success diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt index 29da990..21c147a 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt @@ -5,6 +5,7 @@ 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.exception.ChannelLimitExceededException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -123,12 +124,23 @@ class ChannelCreateCommand( ) }, onFailure = { error -> + val messageKey = + when (error) { + is ChannelLimitExceededException -> + "channel.create.limitExceeded" + else -> + "channel.create.alreadyExists" + } + val params = + when (error) { + is ChannelLimitExceededException -> + mapOf("limit" to error.limit.toString()) + else -> + mapOf("id" to channelId) + } CommandResult.Failure( MessageFormatter.formatError( - languageManager.getMessage( - "channel.create.alreadyExists", - mapOf("id" to channelId), - ), + languageManager.getMessage(messageKey, params), ), ) }, diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt new file mode 100644 index 0000000..cb040d6 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt @@ -0,0 +1,179 @@ +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.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.format.NamedTextColor +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelInfoCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + companion object { + private const val MAX_MEMBERS_DISPLAY = 10 + } + + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelInfo::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("info") + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val result = execute(context, null) + handleResult(context, result) + }.then( + Commands + .argument("channelId", StringArgumentType.word()) + .suggests { _, builder -> + // Tab completion: suggest all public channel IDs + val channels = channelManager.getAllChannels().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, + channelIdArg: String?, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Determine which channel to show info for + val channelId = + channelIdArg ?: channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.info.noActiveChannel"), + ), + ) + + // Get channel + val channel = + channelManager.getChannel(channelId).getOrElse { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.info.notFound", + mapOf("channelId" to channelId), + ), + ), + ) + } + + // Get members + val members = + channelManager.getChannelMembers(channelId).getOrElse { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.info.error"), + ), + ) + } + + // Get owner name + val ownerName = Bukkit.getOfflinePlayer(channel.ownerId).name ?: channel.ownerId.toString() + + // Display header + sender.sendMessage( + MessageFormatter.format( + languageManager.getMessage("channel.info.header"), + ), + ) + + // Display channel name + sender.sendMessage( + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.name"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(channel.name, NamedTextColor.AQUA)), + ) + + // Display channel ID + sender.sendMessage( + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.id"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(channel.id, NamedTextColor.YELLOW)), + ) + + // Display owner + sender.sendMessage( + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.owner"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(ownerName, NamedTextColor.GOLD)), + ) + + // Display members + val memberNames = + members.mapNotNull { member -> + Bukkit.getOfflinePlayer(member.playerId).name + } + + val membersText = + if (memberNames.size > MAX_MEMBERS_DISPLAY) { + val displayNames = memberNames.take(MAX_MEMBERS_DISPLAY) + val message = + languageManager.getMessage( + "channel.info.membersOmitted", + mapOf("count" to memberNames.size.toString()), + ) + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(displayNames.joinToString(", "), NamedTextColor.WHITE)) + .append(Component.text(" ... ", NamedTextColor.GRAY)) + .append(Component.text("($message)", NamedTextColor.YELLOW)) + } else { + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(memberNames.joinToString(", "), NamedTextColor.WHITE)) + } + + sender.sendMessage(membersText) + + return CommandResult.Success + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelInfoCommand should use build() method instead of buildCommand()", + ) +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt index 2a73199..c7a86ad 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt @@ -5,7 +5,9 @@ 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.ChannelMemberLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException +import dev.m1sk9.lunaticChat.engine.exception.PlayerChannelLimitExceededException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -113,6 +115,26 @@ class ChannelJoinCommand( ), ) } + is ChannelMemberLimitExceededException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.join.channelMemberLimitExceeded", + mapOf("limit" to error.limit.toString()), + ), + ), + ) + } + is PlayerChannelLimitExceededException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.join.playerChannelLimitExceeded", + mapOf("limit" to error.limit.toString()), + ), + ), + ) + } else -> { CommandResult.Failure( MessageFormatter.formatError( diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt index aaf05af..1004baf 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt @@ -1,5 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel +import com.mojang.brigadier.arguments.IntegerArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode @@ -17,7 +18,7 @@ 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 +import kotlin.math.ceil @PlayerOnly class ChannelListCommand( @@ -25,6 +26,10 @@ class ChannelListCommand( private val channelManager: ChannelManager, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { + companion object { + private const val CHANNELS_PER_PAGE = 10 + } + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { val builder = build() return applyMethodPermission("build", builder) @@ -32,15 +37,31 @@ class ChannelListCommand( @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) } + Commands + .literal("list") + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val result = execute(context, 1) + handleResult(context, result) + }.then( + Commands + .argument("page", IntegerArgumentType.integer(1)) + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } - val result = execute(context) - handleResult(context, result) - } + val page = IntegerArgumentType.getInteger(ctx, "page") + val result = execute(context, page) + handleResult(context, result) + }, + ) - private fun execute(ctx: CommandContext): CommandResult { + private fun execute( + ctx: CommandContext, + page: Int, + ): CommandResult { val sender = ctx.requirePlayer() val result = channelManager.getPublicChannels() @@ -53,6 +74,12 @@ class ChannelListCommand( ), ) } else { + val totalPages = ceil(channels.size.toDouble() / CHANNELS_PER_PAGE).toInt() + val currentPage = page.coerceIn(1, totalPages) + val startIndex = (currentPage - 1) * CHANNELS_PER_PAGE + val endIndex = (startIndex + CHANNELS_PER_PAGE).coerceAtMost(channels.size) + val pageChannels = channels.subList(startIndex, endIndex) + sender.sendMessage( MessageFormatter.format( languageManager.getMessage( @@ -62,7 +89,7 @@ class ChannelListCommand( ), ) - channels.forEach { channel -> + pageChannels.forEach { channel -> val memberCountResult = channelManager.getChannelMembers(channel.id) val memberCount = memberCountResult.getOrNull()?.size @@ -97,8 +124,7 @@ class ChannelListCommand( .text(" • ", NamedTextColor.GRAY) .append( Component - .text(channel.name, NamedTextColor.AQUA) - .decorate(TextDecoration.BOLD), + .text(channel.name, NamedTextColor.AQUA), ).append(Component.text(" (", NamedTextColor.GRAY)) .append(Component.text(channel.id, NamedTextColor.YELLOW)) .append(Component.text(")", NamedTextColor.GRAY)) @@ -107,6 +133,55 @@ class ChannelListCommand( sender.sendMessage(channelInfo) } + + // Display pagination footer + if (totalPages > 1) { + val paginationComponent = + Component + .text() + .append(Component.text("Page ", NamedTextColor.GRAY)) + .append(Component.text("$currentPage", NamedTextColor.YELLOW)) + .append(Component.text("/", NamedTextColor.GRAY)) + .append(Component.text("$totalPages", NamedTextColor.YELLOW)) + + if (currentPage > 1) { + paginationComponent + .append(Component.text(" ", NamedTextColor.GRAY)) + .append( + Component + .text("[◀]", NamedTextColor.GREEN) + .clickEvent(ClickEvent.runCommand("/lc channel list ${currentPage - 1}")) + .hoverEvent( + HoverEvent.showText( + Component.text( + "Go to page ${currentPage - 1}", + NamedTextColor.WHITE, + ), + ), + ), + ) + } + + if (currentPage < totalPages) { + paginationComponent + .append(Component.text(" ", NamedTextColor.GRAY)) + .append( + Component + .text("[▶]", NamedTextColor.GREEN) + .clickEvent(ClickEvent.runCommand("/lc channel list ${currentPage + 1}")) + .hoverEvent( + HoverEvent.showText( + Component.text( + "Go to page ${currentPage + 1}", + NamedTextColor.WHITE, + ), + ), + ), + ) + } + + sender.sendMessage(paginationComponent.build()) + } } CommandResult.Success }, diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt index fa03965..571aa83 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt @@ -19,6 +19,7 @@ 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 +import org.bukkit.Bukkit @PlayerOnly class ChannelStatusCommand( @@ -27,6 +28,10 @@ class ChannelStatusCommand( private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { + companion object { + private const val MAX_MEMBERS_DISPLAY = 10 + } + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { val builder = build() return applyMethodPermission("build", builder) @@ -85,6 +90,44 @@ class ChannelStatusCommand( .append(Component.text(")", NamedTextColor.GRAY)) sender.sendMessage(activeText) + + // Display members of active channel + val activeMembers = + channelManager.getChannelMembers(activeChannelId).getOrElse { + emptyList() + } + + if (activeMembers.isNotEmpty()) { + val memberNames = + activeMembers.mapNotNull { member -> + Bukkit.getOfflinePlayer(member.playerId).name + } + + val membersText = + if (memberNames.size > MAX_MEMBERS_DISPLAY) { + val displayNames = memberNames.take(MAX_MEMBERS_DISPLAY) + val message = + languageManager.getMessage( + "channel.info.membersOmitted", + mapOf("count" to memberNames.size.toString()), + ) + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(displayNames.joinToString(", "), NamedTextColor.WHITE)) + .append(Component.text(" ... ", NamedTextColor.GRAY)) + .append(Component.text("($message)", NamedTextColor.YELLOW)) + } else { + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(memberNames.joinToString(", "), NamedTextColor.WHITE)) + } + + sender.sendMessage(membersText) + } } else { sender.sendMessage( Component 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 9aa563a..c53d154 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 @@ -50,6 +50,9 @@ object ConfigManager { channelChat = ChannelChatFeatureConfig( enabled = configFile.getBoolean("features.channelChat.enabled", false), + maxChannelsPerServer = configFile.getInt("features.channelChat.maxChannelsPerServer", 0), + maxMembersPerChannel = configFile.getInt("features.channelChat.maxMembersPerChannel", 0), + maxMembershipPerPlayer = configFile.getInt("features.channelChat.maxMembershipPerPlayer", 0), ), ), messageFormat = 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 index 828bdb3..5b93ac5 100644 --- 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 @@ -2,4 +2,7 @@ package dev.m1sk9.lunaticChat.paper.config.key data class ChannelChatFeatureConfig( val enabled: Boolean, + val maxChannelsPerServer: Int = 0, + val maxMembersPerChannel: Int = 0, + val maxMembershipPerPlayer: Int = 0, ) 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 fe4239d..86ae129 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 @@ -63,11 +63,13 @@ class PlayerPresenceListener( val context = manager.getPlayerChannelContext(player.uniqueId) context?.let { val notification = - languageManager.getMessage( - "channel.notification.login", - mapOf("channelName" to it.channel.name), + MessageFormatter.format( + languageManager.getMessage( + "channel.notification.login", + mapOf("channelName" to it.channel.name), + ), ) - player.sendMessage(Component.text(notification)) + player.sendMessage(notification) } } } diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml index 839e0e3..8efa1fa 100644 --- a/platform-paper/src/main/resources/config.yml +++ b/platform-paper/src/main/resources/config.yml @@ -51,6 +51,12 @@ features: channelChat: # If enabled, channel-based chat functionality will be activated. enabled: false + # Maximum number of channels that can be created per server. Set to 0 for unlimited. + maxChannelsPerServer: 0 + # Maximum number of members allowed in a single channel. Set to 0 for unlimited. + maxMembersPerChannel: 0 + # Maximum number of channels a single player can join. Set to 0 for unlimited. + maxMembershipPerPlayer: 0 # ---------------------------------------------- # --------- Message Format Settings -------- diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml index 925d7e7..5887b25 100644 --- a/platform-paper/src/main/resources/languages/en.yml +++ b/platform-paper/src/main/resources/languages/en.yml @@ -50,11 +50,13 @@ 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" + info: "/lc channel info [channelId] - Show detailed information about a channel" 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" + limitExceeded: "Cannot create channel: server has reached the maximum limit of {limit} channels" list: header: "=== Public Channels ({count}) ===" empty: "No public channels available" @@ -71,6 +73,8 @@ 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" + channelMemberLimitExceeded: "Cannot join channel: channel has reached the maximum member limit of {limit}" + playerChannelLimitExceeded: "Cannot join channel: you have reached the maximum channel membership limit of {limit}" error: "Failed to join channel" leave: success: "Left channel '{channelName}'" @@ -90,6 +94,16 @@ channel: noChannels: "You are not a member of any channels" clickToSwitch: "Click to switch" error: "Failed to retrieve channel status" + info: + header: "=== Channel Information ===" + name: "Name" + id: "ID" + owner: "Owner" + members: "Members" + membersOmitted: "{count} members (showing first 10)" + notFound: "Channel '{channelId}' not found" + noActiveChannel: "You don't have an active channel. Please specify a channel ID" + error: "Failed to retrieve channel information" notification: login: "You are in channel '{channelName}'" diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml index 17b0bf6..768466a 100644 --- a/platform-paper/src/main/resources/languages/ja.yml +++ b/platform-paper/src/main/resources/languages/ja.yml @@ -50,11 +50,13 @@ channel: leave: "/lc channel leave - アクティブチャンネルから退出します" switch: "/lc channel switch <チャンネルID> - 既に参加しているチャンネルに切り替えます" status: "/lc channel status - チャンネルのステータスとメンバーシップを表示します" + info: "/lc channel info [チャンネルID] - チャンネルの詳細情報を表示します" delete: "/lc channel delete <チャンネルID> - チャンネルを削除します" create: success: "チャンネル '{name}' (ID: {id}) を作成しました" alreadyExists: "チャンネルID '{id}' は既に使用されています" invalidId: "無効なチャンネルID '{id}' です。3-30文字の英数字、アンダースコア、ハイフンのみ使用できます" + limitExceeded: "チャンネルを作成できません: サーバーはチャンネル数の上限 ({limit}件) に達しています" list: header: "=== 公開チャンネル ({count}件) ===" empty: "公開チャンネルはありません" @@ -71,6 +73,8 @@ channel: alreadyMember: "既にチャンネル '{channelName}' のメンバーです" notFound: "チャンネル '{channelId}' が見つかりません" privateChannel: "プライベートチャンネルには招待が必要です" + channelMemberLimitExceeded: "チャンネルに参加できません: チャンネルのメンバー数が上限 ({limit}人) に達しています" + playerChannelLimitExceeded: "チャンネルに参加できません: 参加できるチャンネル数の上限 ({limit}件) に達しています" error: "チャンネルへの参加に失敗しました" leave: success: "チャンネル '{channelName}' から退出しました" @@ -90,6 +94,16 @@ channel: noChannels: "どのチャンネルにも参加していません" clickToSwitch: "クリックして切り替え" error: "チャンネルステータスの取得に失敗しました" + info: + header: "=== チャンネル情報 ===" + name: "名前" + id: "ID" + owner: "オーナー" + members: "メンバー" + membersOmitted: "{count}人のメンバー (最初の10人を表示)" + notFound: "チャンネル '{channelId}' が見つかりません" + noActiveChannel: "アクティブなチャンネルがありません。チャンネルIDを指定してください" + error: "チャンネル情報の取得に失敗しました" notification: login: "チャンネル '{channelName}' に入室中です" diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml index b3a9d8f..0ae5cd3 100644 --- a/platform-paper/src/main/resources/paper-plugin.yml +++ b/platform-paper/src/main/resources/paper-plugin.yml @@ -42,5 +42,7 @@ permissions: lunaticchat.spy: default: op - lunaticchat.noticeUpdate: + lunaticchat.noticeupdate: + default: op + lunaticchat.channelbypass: default: op |
