summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-07 12:58:21 +0900
committerSho Sakuma <me@m1sk9.dev>2026-01-07 12:58:21 +0900
commit64153b0481784071de033a36769059f0dbbb5a80 (patch)
treee096261f47d862f428be7748adce67624591db83
parent218209230eb5b41089b8b2a123646f055427842d (diff)
downloadLunaticChat-64153b0481784071de033a36769059f0dbbb5a80.tar.gz
LunaticChat-64153b0481784071de033a36769059f0dbbb5a80.tar.bz2
LunaticChat-64153b0481784071de033a36769059f0dbbb5a80.zip
feat: Add `/tell`, `/reply` command
-rw-r--r--engine/build.gradle.kts1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt93
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt68
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt95
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()
+ }
+}