summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-02-01 01:11:39 +0900
committerSho Sakuma <me@m1sk9.dev>2026-02-01 01:11:39 +0900
commit14dc6dc4d29b6612f1e5fda5ceda187bc1df9d4d (patch)
tree193544787d107a44f30211f952aa1fb81d502355
parent3fa249dfe563040268f656119623e5734b212d93 (diff)
downloadLunaticChat-14dc6dc4d29b6612f1e5fda5ceda187bc1df9d4d.tar.gz
LunaticChat-14dc6dc4d29b6612f1e5fda5ceda187bc1df9d4d.tar.bz2
LunaticChat-14dc6dc4d29b6612f1e5fda5ceda187bc1df9d4d.zip
feat: Support Velocity intergration in Paper module
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt8
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt94
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt163
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt5
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityIntegrationConfig.kt5
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt234
-rw-r--r--platform-paper/src/main/resources/config.yml4
-rw-r--r--platform-paper/src/main/resources/languages/en.yml13
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml12
-rw-r--r--platform-paper/src/main/resources/paper-plugin.yml2
12 files changed, 544 insertions, 0 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 31a6886..aada4b0 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
@@ -10,6 +10,7 @@ import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry
import dev.m1sk9.lunaticChat.paper.command.impl.ReplyCommand
import dev.m1sk9.lunaticChat.paper.command.impl.TellCommand
import dev.m1sk9.lunaticChat.paper.command.impl.lc.LunaticChatCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lcv.VelocityStatusCommand
import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandlerRegistry
import dev.m1sk9.lunaticChat.paper.command.setting.handler.ChannelMessageNoticeSettingHandler
import dev.m1sk9.lunaticChat.paper.command.setting.handler.DirectMessageNoticeSettingHandler
@@ -151,6 +152,13 @@ class LunaticChat :
)
}
+ // Conditionally register /lcv command if Velocity integration is enabled
+ services.velocityConnectionManager?.let { velocityManager ->
+ commandRegistry.registerAll(
+ VelocityStatusCommand(this, velocityManager, services.languageManager),
+ )
+ }
+
commandRegistry.initialize()
}
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 9069bc4..6843dd3 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
@@ -9,6 +9,7 @@ import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
/**
* Container for initialized services.
@@ -25,6 +26,7 @@ import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
* @property chatModeManager Optional (only when channel chat feature is enabled)
* @property channelMessageHandler Optional (only when channel chat feature is enabled)
* @property channelNotificationHandler Optional (only when channel chat feature is enabled)
+ * @property velocityConnectionManager Optional (only when Velocity integration is enabled)
*/
data class ServiceContainer(
val languageManager: LanguageManager,
@@ -36,4 +38,5 @@ data class ServiceContainer(
val chatModeManager: ChatModeManager? = null,
val channelMessageHandler: ChannelMessageHandler? = null,
val channelNotificationHandler: ChannelNotificationHandler? = null,
+ val velocityConnectionManager: VelocityConnectionManager? = 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 70495a4..fe6a4e4 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
@@ -16,8 +16,13 @@ import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import dev.m1sk9.lunaticChat.paper.settings.YamlPlayerSettingsStorage
+import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
import io.ktor.client.HttpClient
+import org.bukkit.event.EventHandler
+import org.bukkit.event.Listener
+import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.plugin.java.JavaPlugin
+import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Logger
import kotlin.time.Duration.Companion.milliseconds
@@ -51,6 +56,8 @@ class ServiceInitializer(
private var channelMessageHandler: ChannelMessageHandler? = null
private var channelNotificationHandler: ChannelNotificationHandler? = null
private var channelMessageLogger: ChannelMessageLogger? = null
+ private var velocityConnectionManager: VelocityConnectionManager? = null
+ private val handshakeCompleted = AtomicBoolean(false)
/**
* Initializes all services in dependency order.
@@ -108,6 +115,14 @@ class ServiceInitializer(
languageManager = languageManager,
)
+ // 6. Initialize Velocity integration (optional)
+ val velocityManager =
+ if (configuration.features.velocityIntegration.enabled) {
+ initializeVelocityIntegration()
+ } else {
+ null
+ }
+
return ServiceContainer(
languageManager = languageManager,
playerSettingsManager = playerSettingsManager,
@@ -118,6 +133,7 @@ class ServiceInitializer(
chatModeManager = chatModeManager,
channelMessageHandler = channelMessageHandler,
channelNotificationHandler = channelNotificationHandler,
+ velocityConnectionManager = velocityManager,
)
}
@@ -286,6 +302,83 @@ class ServiceInitializer(
}
/**
+ * Initializes Velocity integration with handshake on first player join.
+ */
+ private fun initializeVelocityIntegration(): VelocityConnectionManager {
+ val pluginVersion = plugin.pluginMeta.version
+ val manager =
+ VelocityConnectionManager(
+ plugin = plugin,
+ pluginVersion = pluginVersion,
+ logger = logger,
+ )
+ manager.initialize()
+ velocityConnectionManager = manager
+
+ // Register listener for first player join
+ plugin.server.pluginManager.registerEvents(
+ object : Listener {
+ @EventHandler
+ fun onPlayerJoin(event: PlayerJoinEvent) {
+ // Only perform handshake once
+ if (!handshakeCompleted.getAndSet(true)) {
+ // Schedule handshake 1 second after first player joins
+ plugin.server.scheduler.runTaskLater(
+ plugin,
+ Runnable {
+ performVelocityHandshake(event.player, manager)
+ },
+ 20L,
+ )
+ }
+ }
+ },
+ plugin,
+ )
+
+ logger.info("Velocity integration initialized. Waiting for first player join to perform handshake.")
+ return manager
+ }
+
+ /**
+ * Performs handshake with Velocity proxy.
+ */
+ private fun performVelocityHandshake(
+ player: org.bukkit.entity.Player,
+ manager: VelocityConnectionManager,
+ ) {
+ manager
+ .performHandshake(player)
+ .thenAccept { result ->
+ when (result) {
+ is VelocityConnectionManager.HandshakeResult.Success -> {
+ logger.info("Velocity handshake successful with version ${result.velocityVersion}")
+ }
+ is VelocityConnectionManager.HandshakeResult.Error -> {
+ logger.severe("Velocity handshake failed: ${result.message}")
+ logger.severe("Disabling plugin due to Velocity integration failure")
+ plugin.server.scheduler.runTask(
+ plugin,
+ Runnable {
+ plugin.server.pluginManager.disablePlugin(plugin)
+ },
+ )
+ }
+ }
+ }.exceptionally { throwable ->
+ logger.severe("Velocity handshake exception: ${throwable.message}")
+ throwable.printStackTrace()
+ plugin.server.scheduler.runTask(
+ plugin,
+ Runnable {
+ plugin.server.pluginManager.disablePlugin(plugin)
+ },
+ )
+ null
+ }
+ }
+
+ /**
* Schedules periodic tasks such as cache saving.
*/
fun schedulePeriodicTasks() {
@@ -311,5 +404,6 @@ class ServiceInitializer(
services.channelManager?.saveToDisk()
services.chatModeManager?.shutdown()
channelMessageLogger?.shutdown()
+ services.velocityConnectionManager?.shutdown()
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt
new file mode 100644
index 0000000..145272c
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt
@@ -0,0 +1,163 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lcv
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.engine.protocol.ProtocolVersion
+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.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
+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
+
+/**
+ * /lcv status command
+ *
+ * Displays Velocity integration status
+ */
+@Command(
+ name = "lcv",
+ aliases = ["lunaticvelocity"],
+ description = "Velocity integration status",
+)
+@Permission(LunaticChatPermissionNode.VelocityStatus::class)
+@PlayerOnly
+class VelocityStatusCommand(
+ plugin: LunaticChat,
+ private val velocityConnectionManager: VelocityConnectionManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ override val description: String
+ get() = languageManager.getMessage("commandDescription.lcv")
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal(name)
+ .then(
+ 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()
+ val meta = plugin.pluginMeta
+
+ // Paper plugin version
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "velocity.status.paperVersion",
+ mapOf("version" to meta.version),
+ ),
+ ),
+ )
+
+ // Protocol version
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "velocity.status.protocolVersion",
+ mapOf("version" to ProtocolVersion.version),
+ ),
+ ),
+ )
+
+ // Connection state
+ val state = velocityConnectionManager.getState()
+ val stateMessage =
+ when (state) {
+ VelocityConnectionManager.ConnectionState.DISCONNECTED -> {
+ Component.text("Disconnected", NamedTextColor.GRAY)
+ }
+ VelocityConnectionManager.ConnectionState.HANDSHAKING -> {
+ Component.text("Handshaking...", NamedTextColor.YELLOW)
+ }
+ VelocityConnectionManager.ConnectionState.CONNECTED -> {
+ Component.text("Connected", NamedTextColor.GREEN)
+ }
+ VelocityConnectionManager.ConnectionState.FAILED -> {
+ Component.text("Failed", NamedTextColor.RED)
+ }
+ }
+
+ sender.sendMessage(
+ Component
+ .text(languageManager.getMessage("velocity.status.connectionState"))
+ .append(Component.text(": "))
+ .append(stateMessage),
+ )
+
+ // Velocity version (if connected)
+ velocityConnectionManager.getVelocityVersion()?.let { velocityVersion ->
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "velocity.status.velocityVersion",
+ mapOf("version" to velocityVersion),
+ ),
+ ),
+ )
+ }
+
+ // Error message (if failed)
+ velocityConnectionManager.getLastError()?.let { error ->
+ sender.sendMessage(
+ Component
+ .text(languageManager.getMessage("velocity.status.error"))
+ .append(Component.text(": ", NamedTextColor.RED))
+ .append(Component.text(error, NamedTextColor.RED)),
+ )
+ }
+
+ // Live status check (if connected)
+ if (state == VelocityConnectionManager.ConnectionState.CONNECTED) {
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("velocity.status.checkingLiveStatus"),
+ ),
+ )
+
+ velocityConnectionManager
+ .requestStatus(sender)
+ .thenAccept { response ->
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "velocity.status.liveStatusSuccess",
+ mapOf(
+ "version" to response.velocityVersion,
+ "protocol" to response.protocolVersion,
+ "online" to response.online.toString(),
+ ),
+ ),
+ ),
+ )
+ }.exceptionally { throwable ->
+ sender.sendMessage(
+ Component
+ .text(languageManager.getMessage("velocity.status.liveStatusFailed"))
+ .color(NamedTextColor.RED)
+ .append(Component.text(": ${throwable.message}", NamedTextColor.RED)),
+ )
+ null
+ }
+ }
+
+ return CommandResult.Success
+ }
+}
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 1bd969d..164a0a8 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
@@ -5,6 +5,7 @@ import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
import dev.m1sk9.lunaticChat.paper.config.key.JapaneseConversionFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig
+import dev.m1sk9.lunaticChat.paper.config.key.VelocityIntegrationConfig
import dev.m1sk9.lunaticChat.paper.i18n.Language
import org.bukkit.configuration.file.FileConfiguration
@@ -51,6 +52,10 @@ class ConfigManager {
maxMembersPerChannel = configFile.getInt("features.channelChat.maxMembersPerChannel", 0),
maxMembershipPerPlayer = configFile.getInt("features.channelChat.maxMembershipPerPlayer", 0),
),
+ velocityIntegration =
+ VelocityIntegrationConfig(
+ enabled = configFile.getBoolean("features.velocityIntegration.enabled", false),
+ ),
),
messageFormat =
MessageFormatConfig(
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
index 190e00c..044838d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
@@ -4,4 +4,5 @@ data class FeaturesConfig(
val quickReplies: QuickRepliesFeatureConfig,
val japaneseConversion: JapaneseConversionFeatureConfig,
val channelChat: ChannelChatFeatureConfig,
+ val velocityIntegration: VelocityIntegrationConfig,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityIntegrationConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityIntegrationConfig.kt
new file mode 100644
index 0000000..3cf42bf
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityIntegrationConfig.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.paper.config.key
+
+data class VelocityIntegrationConfig(
+ val enabled: Boolean = false,
+)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt
new file mode 100644
index 0000000..8861528
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt
@@ -0,0 +1,234 @@
+package dev.m1sk9.lunaticChat.paper.velocity
+
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec
+import dev.m1sk9.lunaticChat.engine.protocol.ProtocolVersion
+import org.bukkit.entity.Player
+import org.bukkit.plugin.Plugin
+import org.bukkit.plugin.messaging.PluginMessageListener
+import java.util.concurrent.CompletableFuture
+import java.util.logging.Logger
+
+/**
+ * Manages connection with Velocity proxy
+ */
+class VelocityConnectionManager(
+ private val plugin: Plugin,
+ private val pluginVersion: String,
+ private val logger: Logger,
+) : PluginMessageListener {
+ companion object {
+ private const val CHANNEL = "lunaticchat:main"
+ private const val HANDSHAKE_TIMEOUT_SECONDS = 5L
+ }
+
+ /**
+ * Connection state
+ */
+ enum class ConnectionState {
+ DISCONNECTED,
+ HANDSHAKING,
+ CONNECTED,
+ FAILED,
+ }
+
+ /**
+ * Handshake result
+ */
+ sealed class HandshakeResult {
+ data class Success(
+ val velocityVersion: String,
+ ) : HandshakeResult()
+
+ data class Error(
+ val message: String,
+ ) : HandshakeResult()
+ }
+
+ private var state: ConnectionState = ConnectionState.DISCONNECTED
+ private var handshakeFuture: CompletableFuture<HandshakeResult>? = null
+ private var statusFuture: CompletableFuture<PluginMessage.StatusResponse>? = null
+ private var velocityVersion: String? = null
+ private var lastError: String? = null
+
+ /**
+ * Initialize
+ */
+ fun initialize() {
+ plugin.server.messenger.registerOutgoingPluginChannel(plugin, CHANNEL)
+ plugin.server.messenger.registerIncomingPluginChannel(plugin, CHANNEL, this)
+ logger.info("Velocity integration channel registered: $CHANNEL")
+ }
+
+ /**
+ * Performs handshake
+ *
+ * @param player Player to use for sending messages
+ * @return Handshake result
+ */
+ fun performHandshake(player: Player): CompletableFuture<HandshakeResult> {
+ if (state == ConnectionState.HANDSHAKING) {
+ return handshakeFuture ?: CompletableFuture.completedFuture(
+ HandshakeResult.Error("Handshake already in progress"),
+ )
+ }
+
+ state = ConnectionState.HANDSHAKING
+ lastError = null
+
+ val future = CompletableFuture<HandshakeResult>()
+ handshakeFuture = future
+
+ val handshake =
+ PluginMessage.Handshake(
+ pluginVersion = pluginVersion,
+ protocolMajor = ProtocolVersion.MAJOR,
+ protocolMinor = ProtocolVersion.MINOR,
+ protocolPatch = ProtocolVersion.PATCH,
+ )
+
+ val data = PluginMessageCodec.encode(handshake)
+ player.sendPluginMessage(plugin, CHANNEL, data)
+
+ logger.info("Sending handshake to Velocity (Plugin: $pluginVersion, Protocol: ${ProtocolVersion.version})")
+
+ // Timeout handling
+ plugin.server.scheduler.runTaskLater(
+ plugin,
+ Runnable {
+ if (state == ConnectionState.HANDSHAKING) {
+ state = ConnectionState.FAILED
+ lastError = "Handshake timeout (${HANDSHAKE_TIMEOUT_SECONDS}s)"
+ logger.warning("Handshake timeout - Velocity plugin may not be installed")
+ future.complete(HandshakeResult.Error("Handshake timeout"))
+ handshakeFuture = null
+ }
+ },
+ HANDSHAKE_TIMEOUT_SECONDS * 20L,
+ )
+
+ return future
+ }
+
+ /**
+ * Sends status request
+ *
+ * @param player Player to use for sending messages
+ * @return Status response
+ */
+ fun requestStatus(player: Player): CompletableFuture<PluginMessage.StatusResponse> {
+ val future = CompletableFuture<PluginMessage.StatusResponse>()
+
+ if (state != ConnectionState.CONNECTED) {
+ future.completeExceptionally(IllegalStateException("Not connected to Velocity"))
+ return future
+ }
+
+ statusFuture = future
+
+ val statusRequest = PluginMessage.StatusRequest
+ val data = PluginMessageCodec.encode(statusRequest)
+ player.sendPluginMessage(plugin, CHANNEL, data)
+
+ logger.info("Sending status request to Velocity")
+
+ // Status request with timeout
+ plugin.server.scheduler.runTaskLater(
+ plugin,
+ Runnable {
+ if (!future.isDone) {
+ logger.warning("Status request timeout")
+ future.completeExceptionally(Exception("Status request timeout"))
+ statusFuture = null
+ }
+ },
+ 5L * 20L,
+ )
+
+ return future
+ }
+
+ /**
+ * Plugin message received
+ */
+ override fun onPluginMessageReceived(
+ channel: String,
+ player: Player,
+ message: ByteArray,
+ ) {
+ if (channel != CHANNEL) return
+
+ try {
+ val pluginMessage = PluginMessageCodec.decode(message)
+
+ when (pluginMessage) {
+ is PluginMessage.HandshakeResponse -> handleHandshakeResponse(pluginMessage)
+ is PluginMessage.StatusResponse -> handleStatusResponse(pluginMessage)
+ else -> logger.warning("Unexpected message type: ${pluginMessage::class.simpleName}")
+ }
+ } catch (e: Exception) {
+ logger.severe("Failed to decode plugin message: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * Handles handshake response
+ */
+ private fun handleHandshakeResponse(response: PluginMessage.HandshakeResponse) {
+ val future = handshakeFuture ?: return
+
+ if (response.compatible) {
+ state = ConnectionState.CONNECTED
+ velocityVersion = response.velocityVersion
+ logger.info("Successfully connected to Velocity (version: ${response.velocityVersion})")
+ future.complete(HandshakeResult.Success(response.velocityVersion))
+ } else {
+ state = ConnectionState.FAILED
+ lastError = response.error ?: "Unknown compatibility error"
+ logger.severe("Velocity handshake failed: ${response.error}")
+ future.complete(HandshakeResult.Error(response.error ?: "Unknown error"))
+ }
+
+ handshakeFuture = null
+ }
+
+ /**
+ * Handles status response
+ */
+ private fun handleStatusResponse(response: PluginMessage.StatusResponse) {
+ logger.info("Received status response from Velocity: version=${response.velocityVersion}, protocol=${response.protocolVersion}, online=${response.online}")
+
+ val future = statusFuture
+ if (future != null) {
+ future.complete(response)
+ statusFuture = null
+ } else {
+ logger.warning("Received status response but no future was waiting for it")
+ }
+ }
+
+ /**
+ * Shutdown
+ */
+ fun shutdown() {
+ plugin.server.messenger.unregisterOutgoingPluginChannel(plugin, CHANNEL)
+ plugin.server.messenger.unregisterIncomingPluginChannel(plugin, CHANNEL)
+ logger.info("Velocity integration channel unregistered")
+ }
+
+ /**
+ * Gets current connection state
+ */
+ fun getState(): ConnectionState = state
+
+ /**
+ * Gets Velocity version (if connected)
+ */
+ fun getVelocityVersion(): String? = velocityVersion
+
+ /**
+ * Gets last error message
+ */
+ fun getLastError(): String? = lastError
+}
diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml
index 0889070..7968717 100644
--- a/platform-paper/src/main/resources/config.yml
+++ b/platform-paper/src/main/resources/config.yml
@@ -65,6 +65,10 @@ features:
retentionDays: 30
# Maximum size of a single log file in megabytes. Files exceeding this size will stop accepting new entries.
maxFileSizeMB: 100
+ velocityIntegration:
+ # If enabled, enables integration with Velocity proxy plugin.
+ # This allows Paper and Velocity instances to communicate and verify compatibility.
+ enabled: false
# ----------------------------------------------
# --------- Message Format Settings --------
diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml
index 767a9b3..d8d11fc 100644
--- a/platform-paper/src/main/resources/languages/en.yml
+++ b/platform-paper/src/main/resources/languages/en.yml
@@ -18,6 +18,7 @@ commandDescription:
reply: "Reply to the player who last sent/received a direct message"
tell: "Send a direct message to another player"
lc: "LunaticChat Main Command"
+ lcv: "LunaticChat Velocity Integration Command"
directMessage:
noticeStatus: "Direct message notifications are currently {toggle}"
@@ -196,3 +197,15 @@ general:
toggle:
off: "Disabled"
on: "Enabled"
+
+velocity:
+ status:
+ paperVersion: "Paper Plugin Version: v{version}"
+ velocityVersion: "Velocity Plugin Version: v{version}"
+ protocolVersion: "Protocol Version: v{version}"
+ connectionState: "Connection State"
+ error: "Error"
+ checkingLiveStatus: "Checking live status..."
+ liveStatusSuccess: "Live status: Velocity v{version}, Protocol v{protocol}, Online: {online}"
+ liveStatusFailed: "Live status check failed"
+
diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml
index a6f2526..2300754 100644
--- a/platform-paper/src/main/resources/languages/ja.yml
+++ b/platform-paper/src/main/resources/languages/ja.yml
@@ -18,6 +18,7 @@ commandDescription:
notice: "ダイレクトメッセージ通知のオン/オフを切り替えます"
chNotice: "チャンネルメッセージ通知のオン/オフを切り替えます"
lc: "LunaticChat のメインコマンド"
+ lcv: "LunaticChat Velocity 連携コマンド"
directMessage:
noticeToggle: "ダイレクトメッセージ通知を{toggle}にしました"
@@ -196,3 +197,14 @@ general:
toggle:
on: "有効"
off: "無効"
+
+velocity:
+ status:
+ paperVersion: "Paper プラグインバージョン: v{version}"
+ velocityVersion: "Velocity プラグインバージョン: v{version}"
+ protocolVersion: "プロトコルバージョン: v{version}"
+ connectionState: "接続状態"
+ error: "エラー"
+ checkingLiveStatus: "ライブステータスを確認中..."
+ liveStatusSuccess: "ライブステータス: Velocity v{version}, プロトコル v{protocol}, オンライン: {online}"
+ liveStatusFailed: "ライブステータスチェックに失敗しました"
diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml
index 38d2026..f8a804b 100644
--- a/platform-paper/src/main/resources/paper-plugin.yml
+++ b/platform-paper/src/main/resources/paper-plugin.yml
@@ -60,3 +60,5 @@ permissions:
default: op
lunaticchat.channelbypass:
default: op
+ lunaticchat.command.lcv.status:
+ default: op