summaryrefslogtreecommitdiff
path: root/platform-paper/src
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-25 17:28:04 +0900
committerSho Sakuma <me@m1sk9.dev>2026-01-26 00:35:42 +0900
commit1dc724dbb96a0d1fdc36c5105538e67efa5b0b87 (patch)
treeff0481301e10fb8419fb7c6d2c2ab5689a9fe225 /platform-paper/src
parentd35eff7d10d6bebd5231e7a5d59a8a13f241e1ee (diff)
downloadLunaticChat-1dc724dbb96a0d1fdc36c5105538e67efa5b0b87.tar.gz
LunaticChat-1dc724dbb96a0d1fdc36c5105538e67efa5b0b87.tar.bz2
LunaticChat-1dc724dbb96a0d1fdc36c5105538e67efa5b0b87.zip
feat: Add channel command
Diffstat (limited to 'platform-paper/src')
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt6
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt6
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt41
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelManager.kt119
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/channel/ChannelMembershipManager.kt153
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt183
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt48
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt142
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt102
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt132
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt85
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt127
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt193
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt130
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt7
-rw-r--r--platform-paper/src/main/resources/languages/en.yml50
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml50
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からダウンロードできます"