diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-09 20:44:56 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-09 20:44:56 +0900 |
| commit | b1c44dc64111710d4197d34e17e912e4f291602e (patch) | |
| tree | 6d9f70d4d01b6689b72848567d5d94d23d2a8eb9 | |
| parent | ce65a31a598884753559e3657de259d66e156bb9 (diff) | |
| parent | da8fac8bdb04213c423d11abd75c0f1a054a02eb (diff) | |
| download | LunaticChat-b1c44dc64111710d4197d34e17e912e4f291602e.tar.gz LunaticChat-b1c44dc64111710d4197d34e17e912e4f291602e.tar.bz2 LunaticChat-b1c44dc64111710d4197d34e17e912e4f291602e.zip | |
Merge pull request #13 from m1sk9/feat/add-command-system
feat: Add LunaticChat command system
27 files changed, 789 insertions, 72 deletions
@@ -23,8 +23,8 @@ _[Supports Minecraft 1.21.X](https://minecraft.wiki/w/Java_Edition_version_histo ## Todo -- [ ] 1on1 Direct Messaging System -- [ ] Quick Reply Functionality +- [x] 1on1 Direct Messaging System +- [x] Quick Reply Functionality - [ ] Romaji to Japanese Conversion - [ ] Velocity Support - [ ] Channel Chat System diff --git a/docker/compose.yaml b/docker/compose.yaml index 536242a..c3814d1 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -1,13 +1,13 @@ services: minecraft: image: itzg/minecraft-server:java21 - container_name: runecore-debug-server + container_name: debug-server user: "0:0" ports: - "25565:25565" - "25575:25575" volumes: - - minecraft-data:/data + - lunatic-debug-minecraft-data:/data - ../platform-paper/build/libs:/plugins:ro - ./plugins:/data/plugins - ./bukkit.yml:/data/bukkit.yml @@ -40,4 +40,4 @@ services: stdin_open: true volumes: - minecraft-data: + lunatic-debug-minecraft-data: diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts index e1b7c51..7afc78f 100644 --- a/engine/build.gradle.kts +++ b/engine/build.gradle.kts @@ -4,6 +4,5 @@ plugins { } dependencies { - // JSON シリアライゼーション implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") } diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/RequirePermissionException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/RequirePermissionException.kt new file mode 100644 index 0000000..dace909 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/RequirePermissionException.kt @@ -0,0 +1,12 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode + +/** + * Exception thrown when required permissions are missing. + * + * @param permissions The list of missing permissions. + */ +class RequirePermissionException( + permissions: List<LunaticChatPermissionNode>, +) : Exception("Missing required permissions: ${permissions.joinToString { it.permissionNode }}") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt new file mode 100644 index 0000000..304d531 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt @@ -0,0 +1,11 @@ +package dev.m1sk9.lunaticChat.engine.permission + +sealed class LunaticChatPermissionNode( + val permissionNode: String, +) { + object Tell : LunaticChatPermissionNode("lunaticchat.command.tell") + + object Reply : LunaticChatPermissionNode("lunaticchat.command.reply") + + object Spy : LunaticChatPermissionNode("lunaticchat.spy") +} diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 315061f..4624a4b 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -12,19 +12,21 @@ repositories { } dependencies { - // engine モジュールを使用 api(project(":engine")) - - // Paper API compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + implementation("org.jetbrains.kotlin:kotlin-reflect") } tasks { shadowJar { - archiveClassifier.set("all") + archiveClassifier.set("") archiveBaseName.set("LunaticChat") } + jar { + enabled = false + } + runServer { minecraftVersion("1.21") } 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 360ebb9..00f2eb9 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,9 +1,22 @@ package dev.m1sk9.lunaticChat.paper +import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry +import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler +import dev.m1sk9.lunaticChat.paper.command.impl.ReplyCommand +import dev.m1sk9.lunaticChat.paper.command.impl.TellCommand +import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager import dev.m1sk9.lunaticChat.paper.config.ConfigManager +import dev.m1sk9.lunaticChat.paper.listener.PlayerPresenceListener +import org.bukkit.event.Listener import org.bukkit.plugin.java.JavaPlugin -class LunaticChat : JavaPlugin() { +class LunaticChat : + JavaPlugin(), + Listener { + lateinit var directMessageHandler: DirectMessageHandler + + private lateinit var commandRegistry: CommandRegistry + override fun onEnable() { saveDefaultConfig() val lunaticChatConfiguration = ConfigManager.loadConfiguration(config) @@ -11,9 +24,21 @@ class LunaticChat : JavaPlugin() { if (lunaticChatConfiguration.debug) { logger.warning("LunaticChat is running in debug mode.") logger.info("Debug: $lunaticChatConfiguration") - // TODO: Enable debug features } + directMessageHandler = DirectMessageHandler() + + commandRegistry = + CommandRegistry(this) + .registerAll( + TellCommand(this, directMessageHandler), + ReplyCommand(this, directMessageHandler), + ) + commandRegistry.initialize() + + server.pluginManager.registerEvents(SpyPermissionManager, this) + server.pluginManager.registerEvents(PlayerPresenceListener(), this) + logger.info("LunaticChat enabled.") } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/Command.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/Command.kt new file mode 100644 index 0000000..f16dd20 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/Command.kt @@ -0,0 +1,16 @@ +package dev.m1sk9.lunaticChat.paper.command.annotation + +/** + * Marks a class as a LunaticChat command. + * + * @param name The primary command name (without leading slash) + * @param aliases List of alternative command names + * @param description Human-readable description for help text + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Command( + val name: String, + val aliases: Array<String> = [], + val description: String = "", +) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/Permission.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/Permission.kt new file mode 100644 index 0000000..d96e1b5 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/Permission.kt @@ -0,0 +1,15 @@ +package dev.m1sk9.lunaticChat.paper.command.annotation + +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import kotlin.reflect.KClass + +/** + * Restricts command execution to senders with the specified permission. + * + * @param value The permission node required to execute this command + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Permission( + val value: KClass<out LunaticChatPermissionNode>, +) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/PlayerOnly.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/PlayerOnly.kt new file mode 100644 index 0000000..0669e28 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/annotation/PlayerOnly.kt @@ -0,0 +1,9 @@ +package dev.m1sk9.lunaticChat.paper.command.annotation + +/** + * Restricts command execution to players only. + * Console execution will be rejected with an appropriate message. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class PlayerOnly diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt new file mode 100644 index 0000000..7f1bb15 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt @@ -0,0 +1,68 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import io.papermc.paper.command.brigadier.CommandSourceStack +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +/** + * Wrapper for Brigadier command context providing type-safe access + * to command sender and arguments. + */ +class CommandContext( + private val sourceStack: CommandSourceStack, +) { + /** The raw command sender (player, console, or other) */ + val sender: CommandSender + get() = sourceStack.sender + + /** The player if sender is a player, null otherwise */ + val player: Player? + get() = sender as? Player + + /** Whether the sender is a player */ + val isPlayer: Boolean + get() = sender is Player + + /** + * Requires the sender to be a player. + * + * @return The player + * @throws IllegalStateException if sender is not a player + */ + fun requirePlayer(): Player = player ?: throw IllegalStateException("This command can only be executed by a player") + + /** + * Sends a message to the command sender. + * + * @param message The message component to send + */ + fun reply(message: Component) { + sender.sendMessage(message) + } + + /** + * Sends a message with a click event to the command sender. + * + * @param message The message component to send + * @param event The click event to attach to the message + */ + fun replyWithEvent( + message: Component, + event: ClickEvent, + ) { + message + .clickEvent(event) + .let { sender.sendMessage(it) } + } + + /** + * Sends a plain text message to the command sender. + * + * @param message The plain text message to send + */ + fun replyPlain(message: String) { + sender.sendPlainMessage(message) + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandRegistry.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandRegistry.kt new file mode 100644 index 0000000..11929cf --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandRegistry.kt @@ -0,0 +1,65 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import dev.m1sk9.lunaticChat.paper.LunaticChat +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents +import java.util.logging.Logger + +/** + * Central registry for all LunaticChat commands. + * Handles registration with Paper's LifecycleEventManager. + */ +class CommandRegistry( + private val plugin: LunaticChat, +) { + private val logger: Logger = plugin.logger + private val commands: MutableList<LunaticCommand> = mutableListOf() + + /** + * Registers a command to be registered with the server. + * Must be called before initialize(). + */ + fun register(command: LunaticCommand): CommandRegistry { + commands.add(command) + return this + } + + /** + * Registers multiple commands at once. + */ + fun registerAll(vararg commands: LunaticCommand): CommandRegistry { + commands.forEach { register(it) } + return this + } + + /** + * Initializes command registration with Paper's LifecycleEventManager. + * This should be called from the plugin's onEnable(). + */ + fun initialize() { + val lifecycleManager = plugin.lifecycleManager + + lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> + val registrar = event.registrar() + + for (command in commands) { + try { + val builtCommand = command.buildWithChecks().build() + + registrar.register( + builtCommand, + command.description, + command.aliases, + ) + + logger.info( + "Registered command: /${command.name} " + + "(aliases: ${command.aliases.joinToString(", ")})", + ) + } catch (e: Exception) { + logger.severe("Failed to register command /${command.name}: ${e.message}") + e.printStackTrace() + } + } + } + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandResult.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandResult.kt new file mode 100644 index 0000000..0a2de3d --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandResult.kt @@ -0,0 +1,37 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import net.kyori.adventure.text.Component + +/** + * Represents the result of a command execution. + * Uses Kotlin sealed classes for type-safe result handling. + */ +sealed class CommandResult { + /** Command executed successfully */ + data object Success : CommandResult() + + /** Command executed successfully with a message to display */ + data class SuccessWithMessage( + val message: Component, + ) : CommandResult() + + /** Command failed with an error message */ + data class Failure( + val message: Component, + ) : CommandResult() + + /** Command failed due to invalid usage */ + data class InvalidUsage( + val usageHint: String, + ) : CommandResult() + + /** + * Converts result to Brigadier return value. + * @return 1 for success, 0 for failure (Brigadier convention) + */ + fun toBrigadierResult(): Int = + when (this) { + is Success, is SuccessWithMessage -> 1 + is Failure, is InvalidUsage -> 0 + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt new file mode 100644 index 0000000..07a7823 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt @@ -0,0 +1,110 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.command.annotation.Command +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import io.papermc.paper.command.brigadier.CommandSourceStack +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor + +/** + * Abstract base class for all LunaticChat commands. + * Provides common functionality and enforces consistent command structure. + */ +abstract class LunaticCommand( + protected val plugin: LunaticChat, +) { + private val commandAnnotation: Command by lazy { + this::class.annotations.filterIsInstance<Command>().firstOrNull() + ?: throw IllegalStateException("Command class must be annotated with @Command") + } + + private val permissionAnnotation: Permission? by lazy { + this::class.annotations.filterIsInstance<Permission>().firstOrNull() + } + + private val isPlayerOnly: Boolean by lazy { + this::class.annotations.any { it is PlayerOnly } + } + + /** The primary command name */ + val name: String get() = commandAnnotation.name + + /** Command aliases */ + val aliases: List<String> get() = commandAnnotation.aliases.toList() + + /** Command description for help text */ + val description: String get() = commandAnnotation.description + + /** Required permission node, if any */ + val permission: String? get() = permissionAnnotation?.value?.objectInstance?.permissionNode + + /** + * Build the Brigadier command tree. + * Subclasses implement this to define arguments and execution logic. + * + * @return The command builder with all arguments and executors attached + */ + abstract fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> + + /** + * Wraps the command builder with permission checks. + * Called by CommandRegistry during registration. + */ + fun buildWithChecks(): LiteralArgumentBuilder<CommandSourceStack> { + var builder = buildCommand() + permission?.let { perm -> + builder = + builder.requires { source -> + source.sender.hasPermission(perm) + } + } + + return builder + } + + /** + * Helper method for checking player-only restriction. + * Called at the beginning of execute methods. + */ + protected fun checkPlayerOnly(ctx: CommandContext): CommandResult? { + if (isPlayerOnly && !ctx.isPlayer) { + return CommandResult.Failure( + Component + .text("This command can only be executed by a player.") + .color(NamedTextColor.RED), + ) + } + + return null + } + + /** + * Utility to wrap Brigadier context into LunaticChat CommandContext. + */ + protected fun wrapContext(ctx: com.mojang.brigadier.context.CommandContext<CommandSourceStack>): CommandContext = + CommandContext(ctx.source) + + /** + * Helper for handling command results and sending appropriate messages. + */ + protected fun handleResult( + ctx: CommandContext, + result: CommandResult, + ): Int { + when (result) { + is CommandResult.Success -> {} + is CommandResult.SuccessWithMessage -> ctx.reply(result.message) + is CommandResult.Failure -> ctx.reply(result.message) + is CommandResult.InvalidUsage -> + ctx.reply( + Component + .text("Usage: ${result.usageHint}") + .color(NamedTextColor.RED), + ) + } + return result.toBrigadierResult() + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt new file mode 100644 index 0000000..0a28a2d --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt @@ -0,0 +1,103 @@ +package dev.m1sk9.lunaticChat.paper.command.handler + +import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager +import dev.m1sk9.lunaticChat.paper.config.ConfigManager +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +/** + * Manages direct message state including reply targets. + * Tracks the last player who messaged each player for /reply functionality. + */ +class DirectMessageHandler { + private var lunaticChatConfiguration = ConfigManager.getConfiguration() + + private val lastMessager: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap() + private val lastRecipient: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap() + + /** + * Records a direct message between two players. + * Updates both the sender's last recipient and the receiver's last messager. + */ + fun recordMessage( + sender: Player, + recipient: Player, + ) { + lastRecipient[sender.uniqueId] = recipient.uniqueId + lastMessager[recipient.uniqueId] = sender.uniqueId + } + + /** + * Gets the player to reply to. + * First checks if someone has messaged this player, otherwise falls back + * to the last person they messaged. + */ + fun getReplyTarget(player: Player): Player? { + val messager = lastMessager[player.uniqueId]?.let { Bukkit.getPlayer(it) } + if (messager != null && messager.isOnline) { + return messager + } + + val recipient = lastRecipient[player.uniqueId]?.let { Bukkit.getPlayer(it) } + if (recipient != null && recipient.isOnline) { + return recipient + } + + return null + } + + /** + * Clears message history for a player (called on disconnect). + */ + fun clearPlayer(player: Player) { + lastMessager.remove(player.uniqueId) + lastRecipient.remove(player.uniqueId) + } + + /** + * Sends a direct message from one player to another. + * Handles formatting and recording the conversation. + * + * @return true if message was sent successfully + */ + fun sendDirectMessage( + sender: Player, + recipient: Player, + message: String, + ): Boolean { + recordMessage(sender, recipient) + val format = lunaticChatConfiguration.messageFormat.directMessageFormat + val formattedMessage = formatMessage(format, sender.name, recipient.name, message) + + SpyPermissionManager.getDirectMessageSpyPlayers().values.forEach { + if (it.isOnline && it.uniqueId != sender.uniqueId && it.uniqueId != recipient.uniqueId) { + it.sendMessage(formattedMessage) + } + } + + sender.sendMessage(formattedMessage) + recipient.sendMessage(formattedMessage) + return true + } + + private fun formatMessage( + format: String, + senderName: String, + recipientName: String, + message: String, + ): Component { + val text = + format + .replace("{sender}", senderName) + .replace("{recipient}", recipientName) + .replace("{message}", message) + + return Component + .text(text) + .clickEvent(ClickEvent.suggestCommand("/tell $senderName ")) + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt new file mode 100644 index 0000000..37e0a82 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt @@ -0,0 +1,68 @@ +package dev.m1sk9.lunaticChat.paper.command.impl + +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.command.annotation.Command +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.CommandResult +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler +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 + +/** + * Quick reply command for responding to the last person who messaged you. + * Usage: /reply <message> + */ +@Command( + name = "reply", + aliases = ["r"], + description = "Reply to the last person who messaged you", +) +@Permission(LunaticChatPermissionNode.Reply::class) +@PlayerOnly +class ReplyCommand( + plugin: LunaticChat, + private val dmHandler: DirectMessageHandler, +) : LunaticCommand(plugin) { + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal(name) + .then( + Commands + .argument("message", StringArgumentType.greedyString()) + .executes { ctx -> + val context = wrapContext(ctx) + + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + val message = StringArgumentType.getString(ctx, "message") + val result = execute(context, message) + + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + message: String, + ): CommandResult { + val sender = ctx.requirePlayer() + val target = + dmHandler.getReplyTarget(sender) + ?: return CommandResult.Failure( + Component + .text("You have no one to reply to.") + .color(NamedTextColor.RED), + ) + + dmHandler.sendDirectMessage(sender, target, message) + + return CommandResult.Success + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt new file mode 100644 index 0000000..54ab036 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt @@ -0,0 +1,95 @@ +package dev.m1sk9.lunaticChat.paper.command.impl + +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.command.annotation.Command +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.CommandResult +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler +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 +import java.util.concurrent.CompletableFuture + +/** + * Direct messaging command. + * Usage: /tell <player> <message> + */ +@Command( + name = "tell", + aliases = ["t", "msg", "m", "w", "whisper"], + description = "Send a private message to another player", +) +@Permission(LunaticChatPermissionNode.Tell::class) +@PlayerOnly +class TellCommand( + plugin: LunaticChat, + private val directMessageHandler: DirectMessageHandler, +) : LunaticCommand(plugin) { + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal(name) + .then( + Commands + .argument("player", StringArgumentType.word()) + .suggests { _, builder -> suggestOnlinePlayers(builder) } + .then( + Commands + .argument("message", StringArgumentType.greedyString()) + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val targetName = StringArgumentType.getString(ctx, "player") + val message = StringArgumentType.getString(ctx, "message") + + val result = execute(context, targetName, message) + handleResult(context, result) + }, + ), + ) + + private fun execute( + ctx: CommandContext, + targetName: String, + message: String, + ): CommandResult { + val sender = ctx.requirePlayer() + val recipient = + Bukkit.getPlayer(targetName) + ?: return CommandResult.Failure( + Component + .text("Player '$targetName' is not online.") + .color(NamedTextColor.RED), + ) + + if (recipient.uniqueId == sender.uniqueId) { + return CommandResult.Failure( + Component + .text("You cannot send a message to yourself.") + .color(NamedTextColor.RED), + ) + } + directMessageHandler.sendDirectMessage(sender, recipient, message) + + return CommandResult.Success + } + + private fun suggestOnlinePlayers(builder: SuggestionsBuilder): CompletableFuture<Suggestions> { + val input = builder.remaining.lowercase() + Bukkit + .getOnlinePlayers() + .filter { it.name.lowercase().startsWith(input) } + .forEach { builder.suggest(it.name) } + return builder.buildFuture() + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/PermissionCollector.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/PermissionCollector.kt new file mode 100644 index 0000000..4edfe34 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/PermissionCollector.kt @@ -0,0 +1,60 @@ +package dev.m1sk9.lunaticChat.paper.common + +import dev.m1sk9.lunaticChat.engine.exception.RequirePermissionException +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import org.bukkit.entity.Player + +@DslMarker +annotation class PermissionDsl + +@PermissionDsl +class PermissionCollector { + internal val permissions = mutableListOf<LunaticChatPermissionNode>() + + operator fun LunaticChatPermissionNode.unaryPlus() { + permissions.add(this) + } +} + +fun LunaticChatPermissionNode.has(player: Player): Boolean = player.hasPermission(this.permissionNode) + +/** + * Checks if the player has at least one of the specified permissions. + * + * @param block A lambda with receiver to collect permissions. + * @return `true` if the player has at least one of the specified permissions. + */ +fun Player.hasAnyPermission(block: PermissionCollector.() -> Unit): Boolean = + PermissionCollector() + .apply(block) + .permissions + .any { it.has(this) } + +/** + * Checks if the player has all the specified permissions. + * + * @param block A lambda with receiver to collect permissions. + * @return `true` if the player has all the specified permissions. + */ +fun Player.hasAllPermission(block: PermissionCollector.() -> Unit): Boolean = + PermissionCollector() + .apply(block) + .permissions + .all { it.has(this) } + +/** + * Requires the player to have at least one of the specified permissions. + * + * @param block A lambda with receiver to collect permissions. + * @return Throws [RequirePermissionException] if the player lacks all specified permissions. + * @throws RequirePermissionException if the player lacks all specified permissions. + */ +fun Player.requirePermission(block: PermissionCollector.() -> Unit) { + val result = + PermissionCollector() + .apply(block) + .permissions + if (!result.any { it.has(this) }) { + throw RequirePermissionException(result) + } +} 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 b12e935..d10a4e6 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 @@ -2,7 +2,6 @@ package dev.m1sk9.lunaticChat.paper.config import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig -import dev.m1sk9.lunaticChat.paper.config.key.VelocityConfig import org.bukkit.configuration.file.FileConfiguration object ConfigManager { @@ -24,29 +23,8 @@ object ConfigManager { directMessageFormat = configFile.getString( "messageFormat.directMessageFormat", - "&7[&e{sender} &7>>] &f{message}", + "§7[§e{sender} §7>> §e{recipient}§7] §f{message}", )!!, - crossServerMessageFormat = - configFile.getString( - "messageFormat.crossServerMessageFormat", - "&7[&e{sender} &7(&b{server}&7)] &f{message}", - )!!, - crossServerDirectMessageFormat = - configFile.getString( - "messageFormat.crossServerDirectMessageFormat", - "&7[&e{sender}@&7(&b{server}&7) >>] &f{message}", - )!!, - ), - velocity = - VelocityConfig( - enabled = configFile.getBoolean("velocity.enabled", false), - serverName = configFile.getString("velocity.serverName", "s1")!!, - crossServerChatEnabled = configFile.getBoolean("velocity.crossServerChatEnabled", false), - crossServerDirectMessagesEnabled = - configFile.getBoolean( - "velocity.crossServerDirectMessagesEnabled", - false, - ), ), debug = configFile.getBoolean("debug", false), ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt index d474de7..2475086 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt @@ -2,11 +2,9 @@ package dev.m1sk9.lunaticChat.paper.config import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig -import dev.m1sk9.lunaticChat.paper.config.key.VelocityConfig data class LunaticChatConfiguration( val features: FeaturesConfig, val messageFormat: MessageFormatConfig, - val velocity: VelocityConfig, val debug: Boolean = false, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt index 52eddae..53d4399 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt @@ -1,7 +1,5 @@ package dev.m1sk9.lunaticChat.paper.config.key data class MessageFormatConfig( - val directMessageFormat: String = "&7[&e{sender} &7>>] &f{message}", - val crossServerMessageFormat: String = "&7[&e{sender} &7(&b{server}&7)] &f{message}", - val crossServerDirectMessageFormat: String = "&7[&e{sender}@&7(&b{server}&7) >>] &f{message}", + val directMessageFormat: String = "§7[§e{sender} §7>> §e{recipient}§7] §f{message}", ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityConfig.kt deleted file mode 100644 index 3932b41..0000000 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.m1sk9.lunaticChat.paper.config.key - -data class VelocityConfig( - val enabled: Boolean, - val serverName: String, - val crossServerChatEnabled: Boolean, - val crossServerDirectMessagesEnabled: Boolean, -) 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 new file mode 100644 index 0000000..ad53929 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt @@ -0,0 +1,16 @@ +package dev.m1sk9.lunaticChat.paper.listener + +import dev.m1sk9.lunaticChat.paper.LunaticChat +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerQuitEvent + +class PlayerPresenceListener : Listener { + @EventHandler(ignoreCancelled = true) + fun onQuit( + lunaticChat: LunaticChat, + event: PlayerQuitEvent, + ) { + lunaticChat.directMessageHandler.clearPlayer(event.player) + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt new file mode 100644 index 0000000..26de098 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt @@ -0,0 +1,54 @@ +package dev.m1sk9.lunaticChat.paper.common + +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +/** + * Manages spy permission cache for direct messages. + * Caches player references to avoid repeated lookups. + */ +object SpyPermissionManager : Listener { + private val directMessageSpyPlayers: ConcurrentHashMap<UUID, Player> = ConcurrentHashMap() + + /** + * Gets all players with spy permission as a map of UUID to Player. + */ + fun getDirectMessageSpyPlayers(): Map<UUID, Player> = directMessageSpyPlayers.toMap() + + /** + * Gets all player UUIDs with spy permission. + */ + fun getDirectMessageSpyPlayerIds(): Set<UUID> = directMessageSpyPlayers.keys + + /** + * Updates the cache of players with direct message spy permission. + * Call this on player join/quit/permission change events. + */ + fun updateSpyCache() { + directMessageSpyPlayers.clear() + Bukkit + .getOnlinePlayers() + .filter { + it.hasAllPermission { + +LunaticChatPermissionNode.Spy + } + }.associateByTo(directMessageSpyPlayers) { it.uniqueId } + } + + @EventHandler(ignoreCancelled = true) + fun onPlayerJoin(e: PlayerJoinEvent) { + updateSpyCache() + } + + @EventHandler(ignoreCancelled = true) + fun onPlayerQuit(event: PlayerQuitEvent) { + directMessageSpyPlayers.remove(event.player.uniqueId) + } +} diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml index e091954..dd5db15 100644 --- a/platform-paper/src/main/resources/config.yml +++ b/platform-paper/src/main/resources/config.yml @@ -38,23 +38,5 @@ features: messageFormat: # Configure the format for direct messages sent via /tell or /msg - directMessageFormat: "&7[&e{sender} &7>>] &f{message}" - # Configure the format for cross-server messages - crossServerMessageFormat: "&7[&e{sender} &7(&b{server}&7)] &f{message}" - # Configure the format for cross-server direct messages - crossServerDirectMessageFormat: "&7[&e{sender}@&7(&b{server}&7) >>] &f{message}" + directMessageFormat: "§7[§e{sender} §7>> §e{recipient}§7] §f{message}" -# ---------------------------------------------- -# ----------- Velocity Settings ------------ -# ---------------------------------------------- - -velocity: - # If enabled, Activate LunaticChat's Velocity integration features. - # This setting overrides all other features; if set to `false`, features such as cross-server chat will become unavailable. - enabled: false - # The name of the server as recognized by the Velocity proxy. - serverName: "s1" - # If enabled, allows cross-server chat functionality. - crossServerChatEnabled: false - # If enabled, allows cross-server direct messaging functionality. - crossServerDirectMessagesEnabled: false diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml index b3aeacc..7ad79d6 100644 --- a/platform-paper/src/main/resources/paper-plugin.yml +++ b/platform-paper/src/main/resources/paper-plugin.yml @@ -7,3 +7,12 @@ load: STARTUP authors: [ m1sk9 ] description: Next-generation channel chat plugin for Paper/Velocity website: lc.m1sk9.dev + +permissions: + lunaticchat.command.tell: + default: true + lunaticchat.command.reply: + default: true + + lunaticchat.spy: + default: op diff --git a/platform-velocity/build.gradle.kts b/platform-velocity/build.gradle.kts index e259a94..6fa7401 100644 --- a/platform-velocity/build.gradle.kts +++ b/platform-velocity/build.gradle.kts @@ -11,12 +11,7 @@ repositories { } dependencies { - // engine モジュールを使用 api(project(":engine")) - - // Velocity API (将来追加予定) - // compileOnly("com.velocitypowered:velocity-api:3.4.0") - // kapt("com.velocitypowered:velocity-api:3.4.0") } tasks { |
