summaryrefslogtreecommitdiff
path: root/platform-paper/src/main/kotlin
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-02-06 18:43:36 +0900
committerSho Sakuma <me@m1sk9.dev>2026-02-06 20:33:37 +0900
commit2b539cb2cefd56ad4a8322b643d4735581e6804d (patch)
tree24e00d5f40f028323081e35ea7a9f2bf56f726c2 /platform-paper/src/main/kotlin
parent5edc4a18217aa9f330eef50372d48ab1eec5becf (diff)
downloadLunaticChat-2b539cb2cefd56ad4a8322b643d4735581e6804d.tar.gz
LunaticChat-2b539cb2cefd56ad4a8322b643d4735581e6804d.tar.bz2
LunaticChat-2b539cb2cefd56ad4a8322b643d4735581e6804d.zip
feat: Add cross-server chat
Diffstat (limited to 'platform-paper/src/main/kotlin')
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt4
-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.kt39
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt20
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/VelocityIntegrationConfig.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt26
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt97
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt193
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt44
10 files changed, 389 insertions, 41 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 aada4b0..42a4c86 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
@@ -31,7 +31,6 @@ import java.util.concurrent.atomic.AtomicBoolean
class LunaticChat :
JavaPlugin(),
Listener {
- // Public API - accessed by commands (maintain backward compatibility)
lateinit var directMessageHandler: DirectMessageHandler
lateinit var languageManager: LanguageManager
var channelManager: ChannelManager? = null
@@ -40,7 +39,6 @@ class LunaticChat :
var channelMessageHandler: ChannelMessageHandler? = null
var channelNotificationHandler: ChannelNotificationHandler? = null
- // Private services
private lateinit var services: ServiceContainer
private lateinit var configuration: LunaticChatConfiguration
private lateinit var serviceInitializer: ServiceInitializer
@@ -166,7 +164,7 @@ class LunaticChat :
* Registers all event listeners.
*/
private fun registerEventListeners() {
- EventListenerRegistry.registerAll(this, services, updateAvailable)
+ EventListenerRegistry.registerAll(this, services, configuration, updateAvailable)
}
/**
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 6843dd3..5ca5e52 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.CrossServerChatManager
import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
/**
@@ -27,6 +28,7 @@ import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
* @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)
+ * @property crossServerChatManager Optional (only when Velocity integration and cross-server chat are enabled)
*/
data class ServiceContainer(
val languageManager: LanguageManager,
@@ -39,4 +41,5 @@ data class ServiceContainer(
val channelMessageHandler: ChannelMessageHandler? = null,
val channelNotificationHandler: ChannelNotificationHandler? = null,
val velocityConnectionManager: VelocityConnectionManager? = null,
+ val crossServerChatManager: CrossServerChatManager? = 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 fe6a4e4..dc015ed 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,6 +16,7 @@ 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.CrossServerChatManager
import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager
import io.ktor.client.HttpClient
import org.bukkit.event.EventHandler
@@ -57,6 +58,7 @@ class ServiceInitializer(
private var channelNotificationHandler: ChannelNotificationHandler? = null
private var channelMessageLogger: ChannelMessageLogger? = null
private var velocityConnectionManager: VelocityConnectionManager? = null
+ private var crossServerChatManager: CrossServerChatManager? = null
private val handshakeCompleted = AtomicBoolean(false)
/**
@@ -123,6 +125,17 @@ class ServiceInitializer(
null
}
+ // 7. Initialize cross-server chat manager (optional)
+ val crossServerManager =
+ if (configuration.features.velocityIntegration.enabled &&
+ configuration.features.velocityIntegration.crossServerGlobalChat &&
+ velocityManager != null
+ ) {
+ initializeCrossServerChatManager(velocityManager)
+ } else {
+ null
+ }
+
return ServiceContainer(
languageManager = languageManager,
playerSettingsManager = playerSettingsManager,
@@ -134,6 +147,7 @@ class ServiceInitializer(
channelMessageHandler = channelMessageHandler,
channelNotificationHandler = channelNotificationHandler,
velocityConnectionManager = velocityManager,
+ crossServerChatManager = crossServerManager,
)
}
@@ -341,6 +355,31 @@ class ServiceInitializer(
}
/**
+ * Initializes cross-server chat manager.
+ *
+ * @param velocityManager The Velocity connection manager
+ * @return The initialized CrossServerChatManager
+ */
+ private fun initializeCrossServerChatManager(velocityManager: VelocityConnectionManager): CrossServerChatManager {
+ val manager =
+ CrossServerChatManager(
+ plugin = plugin,
+ logger = logger,
+ configuration = configuration,
+ cacheSize = configuration.features.velocityIntegration.messageDeduplicationCacheSize,
+ )
+ crossServerChatManager = manager
+
+ // Set the manager in VelocityConnectionManager to handle incoming messages
+ velocityManager.setCrossServerChatManager(manager)
+
+ logger.info(
+ "Cross-server global chat initialized (cache size: ${configuration.features.velocityIntegration.messageDeduplicationCacheSize})",
+ )
+ return manager
+ }
+
+ /**
* Performs handshake with Velocity proxy.
*/
private fun performVelocityHandshake(
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 164a0a8..33952be 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
@@ -55,6 +55,21 @@ class ConfigManager {
velocityIntegration =
VelocityIntegrationConfig(
enabled = configFile.getBoolean("features.velocityIntegration.enabled", false),
+ crossServerGlobalChat =
+ configFile.getBoolean(
+ "features.velocityIntegration.crossServerGlobalChat",
+ false,
+ ),
+ serverName =
+ configFile.getString(
+ "features.velocityIntegration.serverName",
+ "Unknown",
+ ) ?: "Unknown",
+ messageDeduplicationCacheSize =
+ configFile.getInt(
+ "features.velocityIntegration.messageDeduplicationCacheSize",
+ 100,
+ ),
),
),
messageFormat =
@@ -69,6 +84,11 @@ class ConfigManager {
"messageFormat.channelMessageFormat",
"§7[§b#{channel}§7] §e{sender}: §f{message}",
)!!,
+ crossServerGlobalChatFormat =
+ configFile.getString(
+ "messageFormat.crossServerGlobalChatFormat",
+ "§7[§6{server}§7] §e{sender}: §f{message}",
+ )!!,
),
debug = configFile.getBoolean("debug", false),
checkForUpdates = configFile.getBoolean("checkForUpdates", 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 2e623fa..d31ec4e 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
@@ -3,4 +3,5 @@ package dev.m1sk9.lunaticChat.paper.config.key
data class MessageFormatConfig(
val directMessageFormat: String = "§7[§e{sender} §7>> §e{recipient}§7] §f{message}",
val channelMessageFormat: String = "§7[§b#{channel}§7] §e{sender}: §f{message}",
+ val crossServerGlobalChatFormat: String = "§7[§6{server}§7] §e{sender}: §f{message}",
)
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
index 3cf42bf..51a67b6 100644
--- 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
@@ -2,4 +2,7 @@ package dev.m1sk9.lunaticChat.paper.config.key
data class VelocityIntegrationConfig(
val enabled: Boolean = false,
+ val crossServerGlobalChat: Boolean = false,
+ val serverName: String = "Unknown",
+ val messageDeduplicationCacheSize: Int = 100,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
index 1979b6b..b9999bc 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
@@ -3,6 +3,7 @@ package dev.m1sk9.lunaticChat.paper.listener
import dev.m1sk9.lunaticChat.paper.LunaticChat
import dev.m1sk9.lunaticChat.paper.ServiceContainer
import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import java.util.concurrent.atomic.AtomicBoolean
/**
@@ -17,11 +18,13 @@ object EventListenerRegistry {
*
* @param plugin The plugin instance
* @param services The initialized services
+ * @param configuration The plugin configuration
* @param updateAvailable Atomic flag for update availability
*/
fun registerAll(
plugin: LunaticChat,
services: ServiceContainer,
+ configuration: LunaticChatConfiguration,
updateAvailable: AtomicBoolean,
) {
val pluginManager = plugin.server.pluginManager
@@ -40,12 +43,21 @@ object EventListenerRegistry {
plugin,
)
- // Conditionally register chat listener when all required components are available
- if (services.chatModeManager != null &&
- services.channelManager != null &&
- services.channelMessageHandler != null &&
- services.romajiConverter != null
- ) {
+ // Register chat listener when channel chat is enabled OR velocity cross-server chat is enabled
+ // Channel-related services (chatModeManager, channelManager, channelMessageHandler) can be null
+ // when channel chat is disabled, but cross-server chat can still work
+ val shouldRegisterChatListener =
+ (
+ services.chatModeManager != null &&
+ services.channelManager != null &&
+ services.channelMessageHandler != null
+ ) ||
+ (
+ configuration.features.velocityIntegration.enabled &&
+ configuration.features.velocityIntegration.crossServerGlobalChat
+ )
+
+ if (shouldRegisterChatListener) {
pluginManager.registerEvents(
PlayerChatListener(
chatModeManager = services.chatModeManager,
@@ -54,6 +66,8 @@ object EventListenerRegistry {
romajiConverter = services.romajiConverter,
settingsManager = services.playerSettingsManager,
languageManager = services.languageManager,
+ configuration = configuration,
+ crossServerChatManager = services.crossServerChatManager,
),
plugin,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
index 03423e5..c812ae1 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
@@ -4,25 +4,30 @@ import dev.m1sk9.lunaticChat.engine.chat.ChatMode
import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import dev.m1sk9.lunaticChat.paper.velocity.CrossServerChatManager
import io.papermc.paper.event.player.AsyncChatEvent
import kotlinx.coroutines.runBlocking
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
+import org.bukkit.Bukkit
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
class PlayerChatListener(
- private val chatModeManager: ChatModeManager,
- private val channelManager: ChannelManager,
- private val channelMessageHandler: ChannelMessageHandler,
- private val romajiConverter: RomanjiConverter,
+ private val chatModeManager: ChatModeManager?,
+ private val channelManager: ChannelManager?,
+ private val channelMessageHandler: ChannelMessageHandler?,
+ private val romajiConverter: RomanjiConverter?,
private val settingsManager: PlayerSettingsManager,
private val languageManager: LanguageManager,
+ private val configuration: LunaticChatConfiguration,
+ private val crossServerChatManager: CrossServerChatManager?,
) : Listener {
private val plainTextSerializer = PlainTextComponentSerializer.plainText()
@@ -48,24 +53,29 @@ class PlayerChatListener(
}
val effectiveMode =
- if (hasPrefix) {
- val currentMode = chatModeManager.getChatMode(player.uniqueId)
- currentMode.toggle()
+ if (chatModeManager != null) {
+ if (hasPrefix) {
+ val currentMode = chatModeManager.getChatMode(player.uniqueId)
+ currentMode.toggle()
+ } else {
+ chatModeManager.getChatMode(player.uniqueId)
+ }
} else {
- chatModeManager.getChatMode(player.uniqueId)
+ // Default to GLOBAL when chatModeManager is not available
+ ChatMode.GLOBAL
}
// Handle romaji conversion if enabled
// Uses explicit timeout to prevent long blocking (1s max instead of 3s)
// Note: AsyncChatEvent runs on async thread, so runBlocking here doesn't block main thread
val displayMessage =
- if (settings.japaneseConversionEnabled) {
+ if (settings.japaneseConversionEnabled && romajiConverter != null) {
runCatching {
runBlocking {
kotlinx.coroutines
.withTimeoutOrNull(1000) {
romajiConverter
- .convert(messageWithoutPrefix)
+ ?.convert(messageWithoutPrefix)
}?.let { "$messageWithoutPrefix §e($it)" } ?: messageWithoutPrefix
}
}.getOrElse { messageWithoutPrefix }
@@ -76,30 +86,59 @@ class PlayerChatListener(
// Route message based on chat mode
when (effectiveMode) {
ChatMode.GLOBAL -> {
- event.message(Component.text(displayMessage))
+ val velocityIntegrationEnabled = configuration.features.velocityIntegration.enabled
+ val crossServerChatEnabled = configuration.features.velocityIntegration.crossServerGlobalChat
+
+ Bukkit.getLogger().info(
+ "[LunaticChat DEBUG] Chat mode: GLOBAL, " +
+ "velocityEnabled=$velocityIntegrationEnabled, " +
+ "crossServerEnabled=$crossServerChatEnabled, " +
+ "managerNull=${crossServerChatManager == null}",
+ )
+
+ if (velocityIntegrationEnabled && crossServerChatEnabled && crossServerChatManager != null) {
+ // Send to Velocity for cross-server broadcast
+ crossServerChatManager.sendGlobalMessage(
+ player.uniqueId,
+ player.name,
+ displayMessage,
+ )
+
+ // Display as normal chat on the sender's server (no special formatting)
+ event.message(Component.text(displayMessage))
+ } else {
+ // Existing behavior: normal Minecraft chat
+ event.message(Component.text(displayMessage))
+ }
}
ChatMode.CHANNEL -> {
- val hasActiveChannel = channelManager.getPlayerChannel(player.uniqueId) != null
+ // Channel chat requires channelManager and channelMessageHandler
+ if (channelManager != null && channelMessageHandler != null) {
+ val hasActiveChannel = channelManager.getPlayerChannel(player.uniqueId) != null
+
+ if (hasActiveChannel) {
+ // Cancel event and clear all data to prevent other plugins from capturing it
+ // Even MONITOR priority listeners with ignoreCancelled=false won't get useful data
+ // Message will be delivered by ChannelMessageHandler instead
+ // Logging is handled by our own ChannelMessageLogger
+ event.isCancelled = true
+ event.viewers().clear()
+ event.message(Component.empty())
+ channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix)
+ } else {
+ // Auto-fallback to global chat
+ event.message(Component.text(displayMessage))
- if (hasActiveChannel) {
- // Cancel event and clear all data to prevent other plugins from capturing it
- // Even MONITOR priority listeners with ignoreCancelled=false won't get useful data
- // Message will be delivered by ChannelMessageHandler instead
- // Logging is handled by our own ChannelMessageLogger
- event.isCancelled = true
- event.viewers().clear()
- event.message(Component.empty())
- channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix)
+ // Send warning to player
+ player.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("channel.autoFallback"),
+ ),
+ )
+ }
} else {
- // Auto-fallback to global chat
+ // Channel chat not available, fallback to normal chat
event.message(Component.text(displayMessage))
-
- // Send warning to player
- player.sendMessage(
- MessageFormatter.format(
- languageManager.getMessage("channel.autoFallback"),
- ),
- )
}
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt
new file mode 100644
index 0000000..0f4c34e
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerChatManager.kt
@@ -0,0 +1,193 @@
+package dev.m1sk9.lunaticChat.paper.velocity
+
+import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
+import org.bukkit.plugin.Plugin
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.logging.Logger
+
+/**
+ * Manages cross-server global chat messages
+ *
+ * Handles:
+ * - Sending global chat messages to Velocity
+ * - Processing incoming messages from Velocity
+ * - Message deduplication using LRU cache
+ */
+class CrossServerChatManager(
+ private val plugin: Plugin,
+ private val logger: Logger,
+ private val configuration: LunaticChatConfiguration,
+ private val cacheSize: Int = 100,
+) {
+ companion object {
+ private const val CLEANUP_THRESHOLD_MILLIS = 60_000L
+ }
+
+ /**
+ * Cache of recently processed message IDs (messageId -> timestamp)
+ * Used for deduplication
+ */
+ private val processedMessages = ConcurrentHashMap<String, Long>()
+
+ /**
+ * Sends a global chat message to Velocity for cross-server broadcast
+ *
+ * @param playerId Player UUID
+ * @param playerName Player name
+ * @param message Chat message content
+ */
+ fun sendGlobalMessage(
+ playerId: UUID,
+ playerName: String,
+ message: String,
+ ) {
+ try {
+ val messageId = UUID.randomUUID().toString()
+ val serverName = configuration.features.velocityIntegration.serverName
+
+ // Mark as processed immediately to prevent echo
+ processedMessages[messageId] = System.currentTimeMillis()
+
+ val globalChatMessage =
+ PluginMessage.GlobalChatMessage(
+ messageId = messageId,
+ serverName = serverName,
+ playerId = playerId.toString(),
+ playerName = playerName,
+ message = message,
+ )
+
+ // Send to Velocity
+ val player = plugin.server.getPlayer(playerId)
+ if (player != null) {
+ player.sendPluginMessage(
+ plugin,
+ "lunaticchat:main",
+ dev.m1sk9.lunaticChat.engine.protocol.PluginMessageCodec
+ .encode(globalChatMessage),
+ )
+ logger.info("Sent global chat message to Velocity: messageId=$messageId, player=$playerName")
+ } else {
+ logger.warning("Cannot send global chat message: player $playerId not found")
+ }
+
+ // Cleanup old messages if cache is too large
+ if (processedMessages.size > cacheSize) {
+ cleanupOldMessages()
+ }
+ } catch (e: Exception) {
+ logger.severe("Failed to send global chat message: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * Handles incoming global chat message from Velocity
+ *
+ * @param message Global chat message
+ */
+ fun handleIncomingMessage(message: PluginMessage.GlobalChatMessage) {
+ try {
+ // Check if already processed (deduplication)
+ if (!shouldProcessMessage(message.messageId)) {
+ logger.fine("Ignoring duplicate message: messageId=${message.messageId}")
+ return
+ }
+
+ // Mark as processed
+ processedMessages[message.messageId] = System.currentTimeMillis()
+
+ // Broadcast to all players on this server
+ val formattedMessage = formatCrossServerMessage(message)
+
+ plugin.server.scheduler.runTask(
+ plugin,
+ Runnable {
+ plugin.server.onlinePlayers.forEach { player ->
+ player.sendMessage(formattedMessage)
+ }
+ },
+ )
+
+ logger.info(
+ "Broadcasted global chat message from ${message.serverName}: " +
+ "player=${message.playerName}, messageId=${message.messageId}",
+ )
+
+ // Cleanup if needed
+ if (processedMessages.size > cacheSize) {
+ cleanupOldMessages()
+ }
+ } catch (e: Exception) {
+ logger.severe("Failed to handle incoming global chat message: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * Formats a cross-server chat message using the configured format
+ *
+ * @param message Global chat message
+ * @return Formatted Component
+ */
+ private fun formatCrossServerMessage(message: PluginMessage.GlobalChatMessage): Component {
+ val format = configuration.messageFormat.crossServerGlobalChatFormat
+ val formattedText =
+ format
+ .replace("{server}", message.serverName)
+ .replace("{sender}", message.playerName)
+ .replace("{message}", message.message)
+
+ return LegacyComponentSerializer.legacySection().deserialize(formattedText)
+ }
+
+ /**
+ * Checks if a message should be processed (not a duplicate)
+ *
+ * @param messageId Message ID to check
+ * @return true if message should be processed, false if it's a duplicate
+ */
+ private fun shouldProcessMessage(messageId: String): Boolean = !processedMessages.containsKey(messageId)
+
+ /**
+ * Removes old messages from the cache (LRU cleanup)
+ * Keeps only the most recent messages
+ */
+ private fun cleanupOldMessages() {
+ try {
+ val currentTime = System.currentTimeMillis()
+ val cutoffTime = currentTime - CLEANUP_THRESHOLD_MILLIS
+
+ val iterator = processedMessages.entries.iterator()
+ var removedCount = 0
+
+ while (iterator.hasNext()) {
+ val entry = iterator.next()
+ if (entry.value < cutoffTime) {
+ iterator.remove()
+ removedCount++
+ }
+ }
+
+ if (processedMessages.size > cacheSize) {
+ val sortedEntries = processedMessages.entries.sortedBy { it.value }
+ val toRemove = processedMessages.size - cacheSize
+
+ sortedEntries.take(toRemove).forEach { entry ->
+ processedMessages.remove(entry.key)
+ removedCount++
+ }
+ }
+
+ if (removedCount > 0) {
+ logger.fine("Cleaned up $removedCount old messages from deduplication cache")
+ }
+ } catch (e: Exception) {
+ logger.warning("Failed to cleanup old messages: ${e.message}")
+ }
+ }
+}
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
index e77910b..1a7e667 100644
--- 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
@@ -16,6 +16,7 @@ class VelocityConnectionManager(
private val plugin: Plugin,
private val pluginVersion: String,
private val logger: Logger,
+ private var crossServerChatManager: CrossServerChatManager? = null,
) : PluginMessageListener {
companion object {
private const val CHANNEL = "lunaticchat:main"
@@ -61,6 +62,16 @@ class VelocityConnectionManager(
}
/**
+ * Sets the cross-server chat manager
+ * Called after initialization to avoid circular dependency
+ *
+ * @param manager Cross-server chat manager
+ */
+ fun setCrossServerChatManager(manager: CrossServerChatManager) {
+ this.crossServerChatManager = manager
+ }
+
+ /**
* Performs handshake
*
* @param player Player to use for sending messages
@@ -150,6 +161,10 @@ class VelocityConnectionManager(
/**
* Plugin message received
+ *
+ * @param channel plugin message channel
+ * @param player player who sent the message
+ * @param message message byte array
*/
override fun onPluginMessageReceived(
channel: String,
@@ -159,11 +174,10 @@ class VelocityConnectionManager(
if (channel != CHANNEL) return
try {
- val pluginMessage = PluginMessageCodec.decode(message)
-
- when (pluginMessage) {
+ when (val pluginMessage = PluginMessageCodec.decode(message)) {
is PluginMessage.HandshakeResponse -> handleHandshakeResponse(pluginMessage)
is PluginMessage.StatusResponse -> handleStatusResponse(pluginMessage)
+ is PluginMessage.GlobalChatMessage -> handleGlobalChatMessage(pluginMessage)
else -> logger.warning("Unexpected message type: ${pluginMessage::class.simpleName}")
}
} catch (e: Exception) {
@@ -174,6 +188,8 @@ class VelocityConnectionManager(
/**
* Handles handshake response
+ *
+ * @param response Handshake response message
*/
private fun handleHandshakeResponse(response: PluginMessage.HandshakeResponse) {
val future = handshakeFuture ?: return
@@ -195,6 +211,8 @@ class VelocityConnectionManager(
/**
* Handles status response
+ *
+ * @param response Status response message
*/
private fun handleStatusResponse(response: PluginMessage.StatusResponse) {
logger.info(
@@ -211,6 +229,20 @@ class VelocityConnectionManager(
}
/**
+ * Handles global chat message from Velocity
+ *
+ * @param message Global chat message
+ */
+ private fun handleGlobalChatMessage(message: PluginMessage.GlobalChatMessage) {
+ val manager = crossServerChatManager
+ if (manager != null) {
+ manager.handleIncomingMessage(message)
+ } else {
+ logger.warning("Received global chat message but CrossServerChatManager is not initialized")
+ }
+ }
+
+ /**
* Shutdown
*/
fun shutdown() {
@@ -221,16 +253,22 @@ class VelocityConnectionManager(
/**
* Gets current connection state
+ *
+ * @return Connection state
*/
fun getState(): ConnectionState = state
/**
* Gets Velocity version (if connected)
+ *
+ * @return Velocity version or null if not connected
*/
fun getVelocityVersion(): String? = velocityVersion
/**
* Gets last error message
+ *
+ * @return Last error message or null if none
*/
fun getLastError(): String? = lastError
}