diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-07 12:58:21 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-07 12:58:21 +0900 |
| commit | 64153b0481784071de033a36769059f0dbbb5a80 (patch) | |
| tree | e096261f47d862f428be7748adce67624591db83 | |
| parent | 218209230eb5b41089b8b2a123646f055427842d (diff) | |
| download | LunaticChat-64153b0481784071de033a36769059f0dbbb5a80.tar.gz LunaticChat-64153b0481784071de033a36769059f0dbbb5a80.tar.bz2 LunaticChat-64153b0481784071de033a36769059f0dbbb5a80.zip | |
feat: Add `/tell`, `/reply` command
4 files changed, 256 insertions, 1 deletions
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/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..6ad358d --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt @@ -0,0 +1,93 @@ +package dev.m1sk9.lunaticChat.paper.command.handler + +import dev.m1sk9.lunaticChat.paper.config.ConfigManager +import net.kyori.adventure.text.Component +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) + + 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) + } +} 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() + } +} |
