diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-07 12:58:02 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-07 12:58:02 +0900 |
| commit | 218209230eb5b41089b8b2a123646f055427842d (patch) | |
| tree | 8c51f88f17c93ff576d9ca9ec269c6397409df2f | |
| parent | f6f2ec77fa773250b2663491628f14e5baa92f5d (diff) | |
| download | LunaticChat-218209230eb5b41089b8b2a123646f055427842d.tar.gz LunaticChat-218209230eb5b41089b8b2a123646f055427842d.tar.bz2 LunaticChat-218209230eb5b41089b8b2a123646f055427842d.zip | |
feat: Add command system
15 files changed, 359 insertions, 53 deletions
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..437c2b5 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,20 @@ 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.config.ConfigManager +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 +22,20 @@ 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(this, 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..a880443 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/CommandContext.kt @@ -0,0 +1,52 @@ +package dev.m1sk9.lunaticChat.paper.command.core + +import io.papermc.paper.command.brigadier.CommandSourceStack +import net.kyori.adventure.text.Component +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 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/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/listener/PlayerLogoutListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerLogoutListener.kt new file mode 100644 index 0000000..07ee635 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerLogoutListener.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 PlayerLogoutListener : Listener { + @EventHandler + fun onQuit( + lunaticChat: LunaticChat, + event: PlayerQuitEvent, + ) { + lunaticChat.directMessageHandler.clearPlayer(event.player) + } +} 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..db7aac1 100644 --- a/platform-paper/src/main/resources/paper-plugin.yml +++ b/platform-paper/src/main/resources/paper-plugin.yml @@ -7,3 +7,9 @@ load: STARTUP authors: [ m1sk9 ] description: Next-generation channel chat plugin for Paper/Velocity website: lc.m1sk9.dev + +permissions: + lunaticchat.command.tell: + default: not op + lunaticchat.command.reply: + default: not op |
