diff --git a/build.gradle.kts b/build.gradle.kts index feadab68..cdc86f6d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { allprojects { group = "org.sayandev" - version = "1.10.5.20" + version = "1.10.5.20-pubsub2" description = "A modular Kotlin framework for Minecraft: JE" plugins.apply("maven-publish") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4ffaffc3..b73ed88d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,12 +11,12 @@ cloud = "2.0.0" cloud-platform = "2.0.0-beta.10" #cloud-minecraft = "2.0.2-SNAPSHOT" adventure = "4.23.0" -sayanventure = "1.0.2" mysql-connector = "8.4.0" sqlite-jdbc = "3.46.0.0" h2 = "2.2.224" mariadb = "3.3.3" jedis = "5.0.0" +java-websocket = "1.6.0" reflections = "0.10.2" hikari = "5.1.0" guava = "31.1-jre" @@ -28,9 +28,10 @@ libby = "2.0.0-SNAPSHOT" mccoroutines = "2.20.0" kotlinx-coroutines = "1.10.1" caffeine = "3.2.0" +kaml = "0.85.0" # minecraft -adventure-platform = "4.3.4" +adventure-platform = "4.4.1" # bukkit paper = "1.21-R0.1-SNAPSHOT" @@ -65,12 +66,12 @@ cloud-kotlin-extensions = { group = "org.incendo", name = "cloud-kotlin-extensio adventure-api = { group = "net.kyori", name = "adventure-api", version.ref = "adventure" } adventure-text-minimessage = { group = "net.kyori", name = "adventure-text-minimessage", version.ref = "adventure" } adventure-text-serializer-gson = { group = "net.kyori", name = "adventure-text-serializer-gson", version.ref = "adventure" } -sayanventure-api = { group = "org.sayandev.sayanventure", name = "sayanventure", version.ref = "sayanventure" } mysql-connector = { group = "com.mysql", name = "mysql-connector-j", version.ref = "mysql-connector" } sqlite-jdbc = { group = "org.xerial", name = "sqlite-jdbc", version.ref = "sqlite-jdbc" } h2 = { group = "com.h2database", name = "h2", version.ref = "h2" } mariadb = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version.ref = "mariadb" } jedis = { group = "redis.clients", name = "jedis", version.ref = "jedis" } +java-websocket = { group = "org.java-websocket", name = "Java-WebSocket", version.ref = "java-websocket" } reflections = { group = "org.reflections", name = "reflections", version.ref = "reflections" } hikari = { group = "com.zaxxer", name = "HikariCP", version.ref = "hikari" } guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } @@ -83,6 +84,7 @@ exposed-kotlin-datetime = { group = "org.jetbrains.exposed", name = "exposed-kot libby = { group = "com.alessiodp.libby", name = "libby-core", version.ref = "libby" } kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" } caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" } +kaml = { group = "com.charleskorn.kaml", name = "kaml", version.ref = "kaml" } # bukkit paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } @@ -93,7 +95,6 @@ cloud-minecraft-extras = { group = "org.incendo", name = "cloud-minecraft-extras inventoryframework = { group = "com.github.stefvanschie.inventoryframework", name = "IF", version.ref = "inventoryframework" } xseries = { group = "com.github.cryptomorin", name = "XSeries", version.ref = "xseries" } adventure-platform-bukkit = { group = "net.kyori", name = "adventure-platform-bukkit", version.ref = "adventure-platform" } -sayanventure-platform-bukkit = { group = "org.sayandev.sayanventure", name = "sayanventure-bukkit", version.ref = "sayanventure" } libby-bukkit = { group = "com.alessiodp.libby", name = "libby-bukkit", version.ref = "libby" } libby-paper = { group = "com.alessiodp.libby", name = "libby-paper", version.ref = "libby" } pathetic-pathfinder-bukkit = { group = "com.github.patheloper.pathetic", name = "pathetic-mapping", version.ref = "pathetic-pathfinder-bukkit" } @@ -117,7 +118,7 @@ cloud-velocity = { group = "org.incendo", name = "cloud-velocity", version.ref = # proxy - bungeecord bungeecord = { group = "net.md-5", name = "bungeecord-api", version.ref = "bungeecord" } -sayanventure-platform-bungeecord = { group = "org.sayandev.sayanventure", name = "sayanventure-bungeecord", version.ref = "sayanventure" } +adventure-platform-bungeecord = { group = "net.kyori", name = "adventure-platform-bungeecord", version.ref = "adventure-platform" } libby-bungee = { group = "com.alessiodp.libby", name = "libby-bungee", version.ref = "libby" } mccoroutines-bungeecord-api = { group = "com.github.shynixn.mccoroutine", name = "mccoroutine-bungeecord-api", version.ref = "mccoroutines" } mccoroutines-bungeecord-core = { group = "com.github.shynixn.mccoroutine", name = "mccoroutine-bungeecord-core", version.ref = "mccoroutines" } @@ -149,18 +150,21 @@ implementation-core = [ "checker-qual", "javassist", "annotations", + "adventure-api", + "adventure-text-minimessage", + "adventure-text-serializer-gson", # "slf4j-api", "error_prone_annotations", "geantyref", # transitives - end - "kotlin", +# "kotlin", + "kaml", "snakeyaml", "configurate-yaml", "configurate-extra-kotlin", "cloud-core", "cloud-kotlin-extensions", # "cloud-kotlin-coroutines", - "sayanventure-api", "reflections", "kotlin-reflect", "hikari", @@ -175,6 +179,7 @@ implementation-core = [ # "h2", "mariadb", "jedis", + "java-websocket", # "caffeine" ] implementation-bukkit = [ @@ -182,7 +187,7 @@ implementation-bukkit = [ "cloud-minecraft-extras", "inventoryframework", "xseries", - "sayanventure-platform-bukkit", + "adventure-platform-bukkit", "pathetic-pathfinder-bukkit", "mccoroutines-bukkit-api", "mccoroutines-bukkit-core", @@ -201,7 +206,7 @@ implementation-proxy-velocity = [ "cloud-velocity" ] implementation-proxy-bungeecord = [ - "sayanventure-platform-bungeecord", + "adventure-platform-bungeecord", "mccoroutines-bungeecord-api", "mccoroutines-bungeecord-core", ] \ No newline at end of file diff --git a/stickynote-bukkit/build.gradle.kts b/stickynote-bukkit/build.gradle.kts index 9af03dc8..31f5ef6a 100644 --- a/stickynote-bukkit/build.gradle.kts +++ b/stickynote-bukkit/build.gradle.kts @@ -1,7 +1,6 @@ dependencies { api(libs.libby.bukkit) api(libs.libby.paper) - api(libs.sayanventure.platform.bukkit) api(libs.cloud.paper) api(libs.cloud.minecraft.extras) api(libs.inventoryframework) @@ -11,6 +10,7 @@ dependencies { api(libs.mccoroutines.bukkit.core) api(libs.mccoroutines.folia.api) api(libs.mccoroutines.folia.core) + api(libs.adventure.platform.bukkit) compileOnlyApi(libs.authlib) compileOnlyApi(libs.placeholderapi) diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Component_.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Component_.kt index a65b8933..42beb982 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Component_.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Component_.kt @@ -1,7 +1,7 @@ package org.sayandev.stickynote.bukkit.extension -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component fun Component.toNmsComponent(): Any { return MinecraftComponentSerializer.get().serialize(this) diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Player_.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Player_.kt index 7e6b5471..74b0d870 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Player_.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/extension/Player_.kt @@ -2,18 +2,26 @@ package org.sayandev.stickynote.bukkit.extension import org.bukkit.command.CommandSender import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.text.Component -import org.sayandev.sayanventure.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver import org.sayandev.stickynote.bukkit.utils.AdventureUtils fun CommandSender.sendComponent(message: String, vararg placeholder: TagResolver) { AdventureUtils.sendComponent(this, message, *placeholder) } +fun CommandSender.sendComponent(message: Component, vararg placeholder: TagResolver) { + AdventureUtils.sendComponent(this, message, *placeholder) +} + fun Player.sendComponentActionbar(content: String, vararg placeholder: TagResolver) { AdventureUtils.sendComponentActionbar(this, content, *placeholder) } +fun Player.sendComponentActionbar(content: Component, vararg placeholder: TagResolver) { + AdventureUtils.sendComponentActionbar(this, content, *placeholder) +} + fun CommandSender.openBook(title: Component, author: Component, vararg pages: Component) { AdventureUtils.openBook(this, title, author, *pages) } \ No newline at end of file diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/PluginMessagePublisher.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/PluginMessagePublisher.kt index bc66c5af..c6d2310e 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/PluginMessagePublisher.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/PluginMessagePublisher.kt @@ -2,51 +2,57 @@ package org.sayandev.stickynote.bukkit.messaging.publisher import kotlinx.coroutines.CompletableDeferred import org.bukkit.entity.Player -import org.sayandev.stickynote.bukkit.StickyNote import org.sayandev.stickynote.bukkit.messaging.subscriber.PluginMessageSubscribeListener +import org.sayandev.stickynote.bukkit.onlinePlayers import org.sayandev.stickynote.bukkit.plugin -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asJson -import org.sayandev.stickynote.core.messaging.publisher.Publisher - -abstract class PluginMessagePublisher( - namespace: String, - name: String, - val payloadClass: Class

, - val resultClass: Class, +import org.sayandev.stickynote.bukkit.warn +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.Publisher +import org.sayandev.stickynote.core.messaging.SimpleConnectionMeta + +abstract class PluginMessagePublisher

( + messageMeta: MessageMeta, + connectionMeta: SimpleConnectionMeta, val withSubscriber: Boolean -): Publisher( - StickyNote.logger, - namespace, - name +): Publisher( + messageMeta, + connectionMeta, + plugin.logger ) { - var subscriberListener: PluginMessageSubscribeListener? = null + var subscriberListener: PluginMessageSubscribeListener? = null - init { - register(this) + override fun register() { + super.register() registerChannel() } + override fun unregister() { + super.unregister() + unregisterChannel() + } + private fun registerChannel() { - plugin.server.messenger.registerOutgoingPluginChannel(plugin, this.id()) + plugin.server.messenger.registerOutgoingPluginChannel(plugin, messageMeta.id()) if (withSubscriber) { - subscriberListener = PluginMessageSubscribeListener(namespace, name, payloadClass, resultClass, this) + subscriberListener = PluginMessageSubscribeListener(messageMeta, this) } } private fun unregisterChannel() { - plugin.server.messenger.unregisterOutgoingPluginChannel(plugin, this.id()) + plugin.server.messenger.unregisterOutgoingPluginChannel(plugin, messageMeta.id()) if (withSubscriber) { - plugin.server.messenger.unregisterIncomingPluginChannel(plugin, this.id()) + plugin.server.messenger.unregisterIncomingPluginChannel(plugin, messageMeta.id()) } } - suspend fun publish(player: Player, payloadWrapper: PayloadWrapper

): CompletableDeferred { - player.sendPluginMessage(plugin, this.id(), payloadWrapper.asJson().toByteArray()) + suspend fun publish(player: Player, payloadWrapper: PayloadWrapper

): CompletableDeferred { + player.sendPluginMessage(plugin, messageMeta.id(), payloadWrapper.asJson().toByteArray()) return publish(payloadWrapper) } - abstract fun handle(payload: P): S? + abstract override fun handle(payload: P): R? -} \ No newline at end of file +} diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/ProxyPluginMessagePublisher.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/ProxyPluginMessagePublisher.kt index a1e3b911..8476b26d 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/ProxyPluginMessagePublisher.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/publisher/ProxyPluginMessagePublisher.kt @@ -1,18 +1,17 @@ package org.sayandev.stickynote.bukkit.messaging.publisher -abstract class ProxyPluginMessagePublisher( - namespace: String, - name: String, - payloadClass: Class

, - resultClass: Class, -): PluginMessagePublisher( - namespace, - name, - payloadClass, - resultClass, - true +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.SimpleConnectionMeta + +abstract class ProxyPluginMessagePublisher

( + messageMeta: MessageMeta, + connectionMeta: SimpleConnectionMeta, +): PluginMessagePublisher( + messageMeta, + connectionMeta, + false ) { - override fun handle(payload: P): S? { + override fun handle(payload: P): R? { return null } } \ No newline at end of file diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/subscriber/PluginMessageSubscribeListener.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/subscriber/PluginMessageSubscribeListener.kt index 7fb99022..53cb83e2 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/subscriber/PluginMessageSubscribeListener.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/messaging/subscriber/PluginMessageSubscribeListener.kt @@ -4,55 +4,49 @@ import org.bukkit.entity.Player import org.bukkit.plugin.messaging.PluginMessageListener import org.sayandev.stickynote.bukkit.messaging.publisher.PluginMessagePublisher import org.sayandev.stickynote.bukkit.plugin -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asJson -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asPayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.typedPayload -import org.sayandev.stickynote.core.messaging.publisher.Publisher.Companion.HANDLER_LIST +import org.sayandev.stickynote.bukkit.warn +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.Publisher.Companion.HANDLER_LIST -class PluginMessageSubscribeListener( - val namespace: String, - val name: String, - val payloadClass: Class

, - val resultClass: Class, - val publisher: PluginMessagePublisher? +class PluginMessageSubscribeListener

( + val messageMeta: MessageMeta, + val publisher: PluginMessagePublisher? ): PluginMessageListener { - - fun id(): String { - return "$namespace:$name" - } - init { - plugin.server.messenger.registerIncomingPluginChannel(plugin, this.id(), this) + plugin.server.messenger.registerIncomingPluginChannel(plugin, messageMeta.id(), this) } override fun onPluginMessageReceived(channel: String, player: Player, data: ByteArray) { - val result = String(data).asPayloadWrapper() - when (result.state) { - PayloadWrapper.State.FORWARD -> { + val result = String(data).asPayloadWrapper() + when (result.behaviour) { + PayloadBehaviour.FORWARD -> { if (publisher != null) { val wrappedPayload = String(data).asPayloadWrapper

() - val payloadResult = publisher.handle(wrappedPayload.typedPayload(payloadClass)) ?: return - player.sendPluginMessage(plugin, channel, PayloadWrapper(wrappedPayload.uniqueId, payloadResult, PayloadWrapper.State.RESPOND, wrappedPayload.source).asJson().toByteArray()) + val payloadResult = publisher.handle(wrappedPayload.typedPayload(messageMeta.payloadType)) ?: return + player.sendPluginMessage(plugin, channel, PayloadWrapper(wrappedPayload.uniqueId, payloadResult, PayloadBehaviour.RESPONSE, wrappedPayload.source).asJson().toByteArray()) } else { - throw IllegalStateException("tried to handle a payload with state ${result.state}, but it doesn't have a publisher") + throw IllegalStateException("tried to handle a payload with state ${result.behaviour}, but it doesn't have a publisher") } } - PayloadWrapper.State.RESPOND -> { - for (publisher in HANDLER_LIST.filterIsInstance>()) { - if (publisher.id() == channel) { + PayloadBehaviour.RESPONSE -> { + for (publisher in HANDLER_LIST.filterIsInstance>()) { + if (messageMeta.id() == channel) { publisher.payloads[result.uniqueId]?.apply { - //val handle = String(data).asOptionalPayloadWrapper

()?.typedPayload(payloadClass)?.let { publisher.handle(it) } - this.complete(result.typedPayload(resultClass)) + this.complete(result.typedPayload(messageMeta.resultType)) publisher.payloads.remove(result.uniqueId) - } /*?: throw IllegalStateException("No payload found for uniqueId ${result.uniqueId}")*/ + } ?: throw IllegalStateException("No payload found for uniqueId ${result.uniqueId}") } } } else -> { - throw IllegalStateException("a result payload has been received with ${result.state} state, but it doesn't belong here. (payload: ${result})") + throw IllegalStateException("a result payload has been received with ${result.behaviour} state, but it doesn't belong here. (payload: ${result})") } } } -} \ No newline at end of file +} diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/AdventureUtils.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/AdventureUtils.kt index 171e4cca..e2ba6bc0 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/AdventureUtils.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/AdventureUtils.kt @@ -4,17 +4,16 @@ import net.md_5.bungee.api.chat.BaseComponent import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.audience.Audience -import org.sayandev.sayanventure.adventure.inventory.Book -import org.sayandev.sayanventure.adventure.platform.bukkit.BukkitAudiences -import org.sayandev.sayanventure.adventure.text.Component -import org.sayandev.sayanventure.adventure.text.format.TextDecoration -import org.sayandev.sayanventure.adventure.text.minimessage.MiniMessage -import org.sayandev.sayanventure.adventure.text.minimessage.tag.resolver.TagResolver -import org.sayandev.sayanventure.adventure.text.serializer.bungeecord.BungeeComponentSerializer -import org.sayandev.sayanventure.adventure.text.serializer.gson.GsonComponentSerializer -import org.sayandev.sayanventure.adventure.text.serializer.legacy.LegacyComponentSerializer -import org.sayandev.stickynote.bukkit.StickyNote +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.inventory.Book +import net.kyori.adventure.platform.bukkit.BukkitAudiences +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer import org.sayandev.stickynote.bukkit.hook.PlaceholderAPIHook import org.sayandev.stickynote.bukkit.plugin @@ -49,7 +48,16 @@ object AdventureUtils { if (!ServerVersion.isAtLeast(21, 6)) { senderAudience(sender).sendMessage(PlaceholderAPIHook.injectPlaceholders(sender as? Player, message).component(*placeholder)) } else { - sender.sendMessage(PlaceholderAPIHook.injectPlaceholders(sender as? Player, message).component(*placeholder).adventureComponent()) + sender.sendMessage(PlaceholderAPIHook.injectPlaceholders(sender as? Player, message).component(*placeholder)) + } + } + + @JvmStatic + fun sendComponent(sender: CommandSender, message: Component, vararg placeholder: TagResolver) { + if (!ServerVersion.isAtLeast(21, 6)) { + senderAudience(sender).sendMessage(message) + } else { + sender.sendMessage(message) } } @@ -58,7 +66,7 @@ object AdventureUtils { if (!ServerVersion.isAtLeast(21, 6)) { senderAudience(sender).sendMessage(message) } else { - sender.sendMessage(message.adventureComponent()) + sender.sendMessage(message) } } @@ -67,7 +75,16 @@ object AdventureUtils { if (!ServerVersion.isAtLeast(21, 6)) { senderAudience(player).sendActionBar(PlaceholderAPIHook.injectPlaceholders(player, content).component(*placeholder)) } else { - player.sendActionBar(PlaceholderAPIHook.injectPlaceholders(player, content).component(*placeholder).adventureComponent()) + player.sendActionBar(PlaceholderAPIHook.injectPlaceholders(player, content).component(*placeholder)) + } + } + + @JvmStatic + fun sendComponentActionbar(player: Player, content: Component, vararg placeholder: TagResolver) { + if (!ServerVersion.isAtLeast(21, 6)) { + senderAudience(player).sendActionBar(content) + } else { + player.sendActionBar(content) } } @@ -76,7 +93,7 @@ object AdventureUtils { if (!ServerVersion.isAtLeast(21, 6)) { senderAudience(sender).sendActionBar(content) } else { - sender.sendActionBar(content.adventureComponent()) + sender.sendActionBar(content) } } @@ -95,10 +112,10 @@ object AdventureUtils { )) } else { sender.openBook( - net.kyori.adventure.inventory.Book.book( - title.adventureComponent(), - author.adventureComponent(), - *pages.map { it.adventureComponent() }.toTypedArray() + Book.book( + title, + author, + *pages ) ) } @@ -110,42 +127,19 @@ object AdventureUtils { return if (options.removeStartingItalic) Component.empty().decoration(TextDecoration.ITALIC, false).append(component) else component } - @JvmStatic - fun toAdventureComponent(content: String, vararg placeholder: net.kyori.adventure.text.minimessage.tag.resolver.TagResolver): net.kyori.adventure.text.Component { - val component = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(content, *placeholder) - return if (options.removeStartingItalic) net.kyori.adventure.text.Component.empty().decoration(net.kyori.adventure.text.format.TextDecoration.ITALIC, false).append(component) else component - } - @JvmStatic fun toComponent(player: Player?, content: String, vararg placeholder: TagResolver): Component { return miniMessage.deserialize(PlaceholderAPIHook.injectPlaceholders(player, content), *placeholder) } - @JvmStatic - fun toAdventureComponent(player: Player?, content: String, vararg placeholder: net.kyori.adventure.text.minimessage.tag.resolver.TagResolver): net.kyori.adventure.text.Component { - return net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(PlaceholderAPIHook.injectPlaceholders(player, content), *placeholder) - } - fun String.component(vararg placeholder: TagResolver): Component { return toComponent(this, *placeholder) } - fun Component.adventureComponent(): net.kyori.adventure.text.Component { - return net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize(GsonComponentSerializer.gson().serialize(this)) - } - - fun String.adventureComponent(vararg placeholder: net.kyori.adventure.text.minimessage.tag.resolver.TagResolver): net.kyori.adventure.text.Component { - return toAdventureComponent(this, *placeholder) - } - fun String.component(player: Player?, vararg placeholder: TagResolver): Component { return toComponent(player, this, *placeholder) } - fun String.adventureComponent(player: Player?, vararg placeholder: net.kyori.adventure.text.minimessage.tag.resolver.TagResolver): net.kyori.adventure.text.Component { - return toAdventureComponent(player, this, *placeholder) - } - fun Component.legacyString(): String { return legacyAmpersandSerializer.serialize(this) } diff --git a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/ItemUtils.kt b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/ItemUtils.kt index 71ec5126..f4aed6a1 100644 --- a/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/ItemUtils.kt +++ b/stickynote-bukkit/src/main/kotlin/org/sayandev/stickynote/bukkit/utils/ItemUtils.kt @@ -4,7 +4,6 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack import org.sayandev.stickynote.bukkit.StickyNote -import org.sayandev.stickynote.bukkit.utils.AdventureUtils.adventureComponent import org.sayandev.stickynote.bukkit.utils.AdventureUtils.bungeeComponent import org.sayandev.stickynote.bukkit.utils.AdventureUtils.component import org.sayandev.stickynote.bukkit.utils.AdventureUtils.legacyColored @@ -25,12 +24,12 @@ object ItemUtils { fun ItemStack.withDisplayName(displayName: String, placeholders: Map = emptyMap()): ItemStack { if (StickyNote.isPaper && ServerVersion.supports(18)) { this.editMeta { - it.displayName(displayName.adventureComponent(*placeholders.map { Placeholder.parsed(it.key, it.value) }.toTypedArray())) + it.displayName(displayName.component(*placeholders.map { Placeholder.parsed(it.key, it.value) }.toTypedArray())) } } else { if (ServerVersion.supports(16)) { this.itemMeta.let { meta -> - meta.setDisplayNameComponent(displayName.component(*placeholders.map { org.sayandev.sayanventure.adventure.text.minimessage.tag.resolver.Placeholder.parsed(it.key, it.value) }.toTypedArray()).bungeeComponent()) + meta.setDisplayNameComponent(displayName.component(*placeholders.map { net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed(it.key, it.value) }.toTypedArray()).bungeeComponent()) this.itemMeta = meta } } else { @@ -47,13 +46,13 @@ object ItemUtils { if (StickyNote.isPaper && ServerVersion.supports(18)) { this.editMeta { it.lore(lore.map { - it.adventureComponent(*placeholders.map { Placeholder.parsed(it.key, it.value) }.toTypedArray()) + it.component(*placeholders.map { Placeholder.parsed(it.key, it.value) }.toTypedArray()) }) } } else { if (ServerVersion.supports(16)) { this.itemMeta.let { meta -> - meta.loreComponents = lore.map { it.component(*placeholders.map { org.sayandev.sayanventure.adventure.text.minimessage.tag.resolver.Placeholder.parsed(it.key, it.value) }.toTypedArray()).bungeeComponent() } + meta.loreComponents = lore.map { it.component(*placeholders.map { net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed(it.key, it.value) }.toTypedArray()).bungeeComponent() } this.itemMeta = meta } } else { diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/NMSUtils.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/NMSUtils.kt index 95eceb0b..d9a571ba 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/NMSUtils.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/NMSUtils.kt @@ -14,10 +14,10 @@ import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack import org.jetbrains.annotations.ApiStatus -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component -import org.sayandev.sayanventure.adventure.text.format.TextDecoration -import org.sayandev.sayanventure.adventure.text.serializer.gson.GsonComponentSerializer +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import org.sayandev.stickynote.bukkit.nms.accessors.* import org.sayandev.stickynote.bukkit.utils.ServerVersion import org.sayandev.stickynote.core.math.Vector3 diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/PacketUtils.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/PacketUtils.kt index 5f6fbf2a..9a905132 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/PacketUtils.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/PacketUtils.kt @@ -7,8 +7,8 @@ import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.potion.PotionEffect import org.bukkit.potion.PotionEffectType -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component import org.sayandev.stickynote.bukkit.nms.accessors.* import org.sayandev.stickynote.bukkit.nms.enum.* import org.sayandev.stickynote.bukkit.utils.MathUtils diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/Hologram.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/Hologram.kt index a6f33d68..36e21575 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/Hologram.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/Hologram.kt @@ -3,7 +3,7 @@ package org.sayandev.stickynote.bukkit.nms.hologram import org.bukkit.Bukkit.getOnlinePlayers import org.bukkit.Location import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.text.Component import org.sayandev.stickynote.bukkit.extension.toVector3 import org.sayandev.stickynote.bukkit.nms.Viewable import org.sayandev.stickynote.core.math.Vector3 diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/HologramLine.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/HologramLine.kt index 2819834a..552806c3 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/HologramLine.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/hologram/HologramLine.kt @@ -2,7 +2,7 @@ package org.sayandev.stickynote.bukkit.nms.hologram import org.bukkit.Location import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.text.Component import org.sayandev.stickynote.bukkit.extension.toNmsComponent import org.sayandev.stickynote.bukkit.nms.NMSUtils.sendPacket import org.sayandev.stickynote.bukkit.nms.PacketUtils diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/NPC.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/NPC.kt index 9855e7a9..1c53c818 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/NPC.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/NPC.kt @@ -5,9 +5,9 @@ import org.bukkit.Location import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack import org.bukkit.scheduler.BukkitRunnable -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component -import org.sayandev.sayanventure.adventure.text.serializer.legacy.LegacyComponentSerializer +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer import org.sayandev.stickynote.bukkit.extension.toLocation import org.sayandev.stickynote.bukkit.nms.NMSUtils import org.sayandev.stickynote.bukkit.nms.NMSUtils.sendPacket diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/PlayerNPC.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/PlayerNPC.kt index 34d45f04..0bac51b8 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/PlayerNPC.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/PlayerNPC.kt @@ -5,8 +5,8 @@ import org.bukkit.ChatColor import org.bukkit.Location import org.bukkit.World import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component import org.sayandev.stickynote.bukkit.nms.NMSUtils import org.sayandev.stickynote.bukkit.nms.NMSUtils.sendPacket import org.sayandev.stickynote.bukkit.nms.NMSUtils.sendPacketSync diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/entity/display/TextDisplayNPC.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/entity/display/TextDisplayNPC.kt index 20f64901..4344e6af 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/entity/display/TextDisplayNPC.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/npc/entity/display/TextDisplayNPC.kt @@ -1,8 +1,8 @@ package org.sayandev.stickynote.bukkit.nms.npc.entity.display import org.bukkit.Location -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component import org.sayandev.stickynote.bukkit.nms.NMSUtils import org.sayandev.stickynote.bukkit.nms.accessors.Display_TextDisplayAccessor import org.sayandev.stickynote.bukkit.nms.accessors.SynchedEntityDataAccessor diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/Scoreboard.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/Scoreboard.kt index 572490fc..50b7a27a 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/Scoreboard.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/Scoreboard.kt @@ -2,8 +2,8 @@ package org.sayandev.stickynote.bukkit.nms.scoreboard import org.bukkit.ChatColor import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.platform.bukkit.MinecraftComponentSerializer -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.Component import org.sayandev.stickynote.bukkit.nms.NMSUtils.sendPacket import org.sayandev.stickynote.bukkit.nms.PacketUtils import org.sayandev.stickynote.bukkit.nms.Viewable diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/ScoreboardLine.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/ScoreboardLine.kt index 8d1bb997..baf81379 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/ScoreboardLine.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/scoreboard/ScoreboardLine.kt @@ -1,6 +1,6 @@ package org.sayandev.stickynote.bukkit.nms.scoreboard -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.text.Component open class ScoreboardLine( val component: Component, diff --git a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/toast/Toast.kt b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/toast/Toast.kt index 8a3933de..b146d113 100644 --- a/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/toast/Toast.kt +++ b/stickynote-bukkit/stickynote-bukkit-nms/src/main/kotlin/org/sayandev/stickynote/bukkit/nms/toast/Toast.kt @@ -8,9 +8,9 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import kotlinx.coroutines.delay import org.bukkit.entity.Player -import org.sayandev.sayanventure.adventure.text.Component -import org.sayandev.sayanventure.adventure.text.minimessage.tag.resolver.TagResolver -import org.sayandev.sayanventure.adventure.text.serializer.gson.GsonComponentSerializer +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import org.sayandev.stickynote.bukkit.async import org.sayandev.stickynote.bukkit.launch import org.sayandev.stickynote.bukkit.nms.NMSUtils diff --git a/stickynote-core/build.gradle.kts b/stickynote-core/build.gradle.kts index 813d0ff0..12a1fc68 100644 --- a/stickynote-core/build.gradle.kts +++ b/stickynote-core/build.gradle.kts @@ -5,9 +5,9 @@ dependencies { api(libs.cloud.core) api(libs.cloud.kotlin.extensions) // api(libs.cloud.kotlin.coroutines) - compileOnlyApi(libs.adventure.api) - compileOnlyApi(libs.adventure.text.minimessage) - compileOnlyApi(libs.adventure.text.serializer.gson) + api(libs.adventure.api) + api(libs.adventure.text.minimessage) + api(libs.adventure.text.serializer.gson) api(libs.mysql.connector) api(libs.jedis) api(libs.reflections) @@ -15,13 +15,14 @@ dependencies { api(libs.gson) api(libs.kotlin.reflect) api(libs.kotlinx.coroutines) + api(libs.java.websocket) api(libs.mariadb) api(libs.exposed.core) api(libs.exposed.jdbc) // api(libs.exposed.r2dbc) api(libs.exposed.dao) api(libs.exposed.kotlin.datetime) - api(libs.sayanventure.api) + api(libs.kaml) compileOnlyApi(libs.guava) compileOnlyApi(libs.netty.all) diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/command/Command.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/command/Command.kt index 26d843aa..ac3a5a6e 100644 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/command/Command.kt +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/command/Command.kt @@ -9,7 +9,7 @@ import org.incendo.cloud.kotlin.extension.buildAndRegister import org.incendo.cloud.kotlin.extension.commandBuilder import org.incendo.cloud.parser.standard.StringParser import org.incendo.cloud.suggestion.Suggestion -import org.sayandev.sayanventure.adventure.text.Component +import net.kyori.adventure.text.Component import org.sayandev.stickynote.core.command.interfaces.CommandExtension import org.sayandev.stickynote.core.command.interfaces.SenderExtension import org.sayandev.stickynote.core.utils.CoroutineUtils.launch diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/configuration/Config.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/configuration/Config.kt index 6ddbe337..34c8aedc 100644 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/configuration/Config.kt +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/configuration/Config.kt @@ -1,15 +1,18 @@ package org.sayandev.stickynote.core.configuration +import com.charleskorn.kaml.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.encodeToString +import kotlinx.serialization.modules.* import org.spongepowered.configurate.CommentedConfigurationNode import org.spongepowered.configurate.ConfigurationOptions import org.spongepowered.configurate.kotlin.objectMapperFactory import org.spongepowered.configurate.serialize.TypeSerializerCollection -import org.spongepowered.configurate.util.MapFactories import org.spongepowered.configurate.yaml.NodeStyle import org.spongepowered.configurate.yaml.YamlConfigurationLoader import java.io.File import java.nio.file.Path -import java.util.UUID +import java.util.* abstract class Config( @Transient val directory: File, @@ -138,6 +141,55 @@ abstract class Config( if (!file.exists()) return null return fromConfig(file, null) } + + + val yaml = Yaml( + EmptySerializersModule(), + YamlConfiguration( + strictMode = false, + yamlNamingStrategy = YamlNamingStrategy.KebabCase, + encodeDefaults = true, + decodeEnumCaseInsensitive = true, + polymorphismStyle = PolymorphismStyle.Property + ) + ) + + inline fun registerSerializer(serializer: KSerializer) { + yaml.serializersModule.plus(SerializersModule { + this.contextual(serializer) + }) + } + + fun registerSerializersModule(serializer: SerializersModule) { + yaml.serializersModule.plus(serializer) + } + + inline fun unregisterSerializer() { + yaml.serializersModule.overwriteWith(EmptySerializersModule()) + } + + inline fun save(file: File, instance: T, yaml: Yaml = this.yaml) { + if (!file.exists()) { + file.parentFile.mkdirs() + file.createNewFile() + } + file.writeText(yaml.encodeToString(instance)) + } + + @JvmStatic + inline fun fromFile(file: File): T? { + return fromFile(file, EmptySerializersModule()) + } + + @JvmStatic + inline fun fromFile(file: File, serializers: SerializersModule): T? { + if (!file.exists()) return null + return try { + Yaml(serializers, yaml.configuration).decodeFromStream(file.inputStream()) + } catch (e: EmptyYamlDocumentException) { + null + } + } } } \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/ConnectionMeta.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/ConnectionMeta.kt new file mode 100644 index 00000000..bea729e7 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/ConnectionMeta.kt @@ -0,0 +1,5 @@ +package org.sayandev.stickynote.core.messaging + +interface ConnectionMeta { + val timeoutMillis: Long +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/MessageMeta.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/MessageMeta.kt new file mode 100644 index 00000000..35322306 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/MessageMeta.kt @@ -0,0 +1,20 @@ +package org.sayandev.stickynote.core.messaging + +import kotlin.reflect.KClass + +data class MessageMeta

( + val namespace: String, + val name: String, + val payloadType: KClass

, + val resultType: KClass, +) { + fun id(): String { + return "$namespace:$name" + } + + companion object { + inline fun create(namespace: String, name: String): MessageMeta { + return MessageMeta(namespace, name, P::class, S::class) + } + } +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/PayloadBehaviour.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/PayloadBehaviour.kt new file mode 100644 index 00000000..4045cd02 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/PayloadBehaviour.kt @@ -0,0 +1,18 @@ +package org.sayandev.stickynote.core.messaging + +enum class PayloadBehaviour { + /** + * Forwards to all available subscribers. + * Subscribers will handle the message and return a response if provided. + * The result will only be available if one of the subscribers returns a response. + * If none of the subscribers return a response, the payload will be ignored. + */ + FORWARD, + + FORWARD_PROXY, + + /** + * The payload will be sent to the source of the message. + */ + RESPONSE +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/PayloadWrapper.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/PayloadWrapper.kt similarity index 80% rename from stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/PayloadWrapper.kt rename to stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/PayloadWrapper.kt index a69ea10a..a3486b95 100644 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/PayloadWrapper.kt +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/PayloadWrapper.kt @@ -1,26 +1,21 @@ -package org.sayandev.stickynote.core.messaging.publisher +package org.sayandev.stickynote.core.messaging import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonIOException import com.google.gson.TypeAdapter import java.util.* +import kotlin.reflect.KClass data class PayloadWrapper

( val uniqueId: UUID = UUID.randomUUID(), val payload: P, - val state: State = State.FORWARD, + val behaviour: PayloadBehaviour = PayloadBehaviour.FORWARD, var source: String? = null, val target: String? = null, val excludeSource: Boolean = false, ) { - constructor(payload: P, state: State = State.FORWARD): this(UUID.randomUUID(), payload, state) - - enum class State { - PROXY, - FORWARD, - RESPOND, - } + constructor(payload: P, behaviour: PayloadBehaviour = PayloadBehaviour.FORWARD): this(UUID.randomUUID(), payload, behaviour) companion object { private var gson = Gson() @@ -82,12 +77,19 @@ data class PayloadWrapper

( } catch (_: Exception) { null } } - fun

PayloadWrapper<*>.typedPayload(payloadClass: Class

): P { + fun

PayloadWrapper<*>.typedPayload(payloadType: KClass

): P { return try { - gson.fromJson(gson.toJson(this.payload), payloadClass) + gson.fromJson(gson.toJson(this.payload), payloadType.java) } catch (e: JsonIOException) { throw IllegalStateException("Could not convert payload to $this", e) } } + + fun

P.toPayloadWrapper(behaviour: PayloadBehaviour = PayloadBehaviour.FORWARD): PayloadWrapper

{ + return PayloadWrapper( + payload = this, + behaviour = behaviour + ) + } } } \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/Publisher.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/Publisher.kt new file mode 100644 index 00000000..85d5a379 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/Publisher.kt @@ -0,0 +1,61 @@ +package org.sayandev.stickynote.core.messaging + +import kotlinx.coroutines.CompletableDeferred +import java.util.UUID +import java.util.logging.Logger + +abstract class Publisher( + val messageMeta: MessageMeta, + val connectionMeta: M, + val logger: Logger, +) { + + val payloads: MutableMap> = mutableMapOf() + + open suspend fun publish(payloadWrapper: PayloadWrapper

): CompletableDeferred { + val deferred = CompletableDeferred() + + if (!registered()) { + logger.warning("A payload has published without being registered but the publisher is not registered: ${messageMeta}") + return deferred + } + + payloads[payloadWrapper.uniqueId] = deferred + + return deferred + } + + open fun handle(payload: P): R? { + return null + } + + fun isSource(uniqueId: UUID): Boolean { + return HANDLER_LIST.flatMap { publisher -> publisher.payloads.keys }.contains(uniqueId) + } + + open fun register() { + register(this) + } + + open fun unregister() { + unregister(this) + } + + fun registered(): Boolean { + return HANDLER_LIST.contains(this) + } + + companion object { + val HANDLER_LIST = mutableListOf>() + + fun register(publisher: Publisher) { + require(!HANDLER_LIST.contains(publisher)) { "Publisher with id ${publisher.messageMeta.id()} is already registered" } + HANDLER_LIST.add(publisher) + } + + fun unregister(publisher: Publisher) { + HANDLER_LIST.remove(publisher) + } + } + +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/SimpleConnectionMeta.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/SimpleConnectionMeta.kt new file mode 100644 index 00000000..66944b9c --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/SimpleConnectionMeta.kt @@ -0,0 +1,5 @@ +package org.sayandev.stickynote.core.messaging + +data class SimpleConnectionMeta( + override val timeoutMillis: Long = 10000L, +) : ConnectionMeta \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/Subscriber.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/Subscriber.kt new file mode 100644 index 00000000..c691beff --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/Subscriber.kt @@ -0,0 +1,33 @@ +package org.sayandev.stickynote.core.messaging + +import kotlinx.coroutines.Deferred + +abstract class Subscriber

( + val messageMeta: MessageMeta, +) { + + abstract suspend fun onSubscribe(payload: P): Deferred + + fun register() { + register(this) + } + + fun unregister() { + unregister(this) + } + + companion object { + val HANDLER_LIST = mutableListOf>() + + val isVelocity = runCatching { Class.forName("com.velocitypowered.api.proxy.ProxyServer") }.isSuccess + + fun

register(subscriber: Subscriber) { + HANDLER_LIST.add(subscriber) + } + + fun

unregister(subscriber: Subscriber) { + HANDLER_LIST.remove(subscriber) + } + } + +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/Publisher.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/Publisher.kt deleted file mode 100644 index 3444fcd3..00000000 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/Publisher.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.sayandev.stickynote.core.messaging.publisher - -import kotlinx.coroutines.CompletableDeferred -import java.util.* -import java.util.logging.Logger - -abstract class Publisher( - val logger: Logger, - val namespace: String, - val name: String, -) { - - val payloads: MutableMap> = mutableMapOf() - - fun id(): String { - return "$namespace:$name" - } - - open suspend fun publish(payloadWrapper: PayloadWrapper

): CompletableDeferred { - val deferred = CompletableDeferred() - payloads[payloadWrapper.uniqueId] = deferred - - return deferred - } - - fun register() { - register(this) - } - - fun unregister() { - unregister(this) - } - - companion object { - val HANDLER_LIST = mutableListOf>() - - fun register(publisher: Publisher) { - require(!HANDLER_LIST.contains(publisher)) { "Publisher with id ${publisher.id()} is already registered" } - HANDLER_LIST.add(publisher) - } - - fun unregister(publisher: Publisher) { - HANDLER_LIST.remove(publisher) - } - } - -} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/RedisPublisher.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/RedisPublisher.kt deleted file mode 100644 index aec47a08..00000000 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/publisher/RedisPublisher.kt +++ /dev/null @@ -1,112 +0,0 @@ -package org.sayandev.stickynote.core.messaging.publisher - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.delay -import org.sayandev.stickynote.core.coroutine.dispatcher.AsyncDispatcher -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asJson -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asPayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.typedPayload -import org.sayandev.stickynote.core.messaging.redis.RedisConnectionManager -import org.sayandev.stickynote.core.utils.CoroutineUtils.launch -import redis.clients.jedis.JedisPool -import java.util.* -import java.util.logging.Level -import java.util.logging.Logger - -abstract class RedisPublisher( - val dispatcher: CoroutineDispatcher, - val redis: JedisPool, - namespace: String, - name: String, - val payloadClass: Class

, - val resultClass: Class, - logger: Logger -) : Publisher( - logger, - namespace, - name -) { - val channel = "$namespace:$name" - - init { - RedisConnectionManager.registerNamespace(namespace, redis, dispatcher) - } - - fun handleForwardMessage(payloadWrapper: PayloadWrapper) { - if (payloadWrapper.excludeSource && isSource(payloadWrapper.uniqueId)) return - - try { - val typedPayload = payloadWrapper.typedPayload(payloadClass) - val payloadResult = handle(typedPayload) ?: return - - val localJedis = redis.resource - try { - localJedis.publish( - channel.toByteArray(), - PayloadWrapper( - payloadWrapper.uniqueId, - - payloadResult, - PayloadWrapper.State.RESPOND, - payloadWrapper.source, - payloadWrapper.target, - payloadWrapper.excludeSource - ).asJson().toByteArray() - ) - } finally { - localJedis.close() - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Error handling forward message: ${e.message}") - } - } - - fun handleResponseMessage(payloadWrapper: PayloadWrapper) { - try { - val typedResult = payloadWrapper.typedPayload(resultClass) - payloads[payloadWrapper.uniqueId]?.apply { - this.complete(typedResult) - payloads.remove(payloadWrapper.uniqueId) - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Error handling response message: ${e.message}") - } - } - - override suspend fun publish(payloadWrapper: PayloadWrapper

): CompletableDeferred { - val result = super.publish(payloadWrapper) - - launch(dispatcher) { - val localJedis = redis.resource - try { - val published = localJedis.publish(channel.toByteArray(), payloadWrapper.asJson().toByteArray()) - if (published <= 0) { - payloads.remove(payloadWrapper.uniqueId) - return@launch - } - } finally { - localJedis.close() - } - - delay(TIMEOUT_SECONDS * 1000L) - if (result.isActive) { - result.completeExceptionally(IllegalStateException("No response received in $TIMEOUT_SECONDS seconds")) - payloads.remove(payloadWrapper.uniqueId) - } - } - - return result - } - - abstract fun handle(payload: P): S? - - fun isSource(uniqueId: UUID): Boolean { - return HANDLER_LIST.asSequence() - .flatMap { publisher -> publisher.payloads.keys.asSequence() }.contains(uniqueId) - } - - companion object { - const val TIMEOUT_SECONDS = 5L - } -} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisConnectionManager.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisConnectionManager.kt deleted file mode 100644 index b7947380..00000000 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisConnectionManager.kt +++ /dev/null @@ -1,165 +0,0 @@ -package org.sayandev.stickynote.core.messaging.redis - -import kotlinx.coroutines.CoroutineDispatcher -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asPayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.Publisher -import org.sayandev.stickynote.core.messaging.publisher.RedisPublisher -import org.sayandev.stickynote.core.messaging.subscriber.RedisSubscriber -import org.sayandev.stickynote.core.messaging.subscriber.Subscriber -import redis.clients.jedis.JedisPool -import redis.clients.jedis.JedisPubSub -import redis.clients.jedis.exceptions.JedisException -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicBoolean -import java.util.logging.Level -import java.util.logging.Logger - -object RedisConnectionManager { - private val namespaceSubscriptions = ConcurrentHashMap() - private val logger = Logger.getLogger(RedisConnectionManager::class.java.name) - - fun registerNamespace( - namespace: String, - redis: JedisPool, - dispatcher: CoroutineDispatcher - ) { - namespaceSubscriptions.computeIfAbsent(namespace) { - NamespaceSubscription(namespace, redis, dispatcher) - } - } - - fun unregisterNamespace(namespace: String) { - namespaceSubscriptions[namespace]?.shutdown() - namespaceSubscriptions.remove(namespace) - } - - fun getSubscription(namespace: String): NamespaceSubscription? { - return namespaceSubscriptions[namespace] - } - - fun shutdown() { - namespaceSubscriptions.values.forEach { it.shutdown() } - namespaceSubscriptions.clear() - } - - class NamespaceSubscription( - private val namespace: String, - private val redis: JedisPool, - private val dispatcher: CoroutineDispatcher - ) { - private val pattern = "$namespace:*" - private var subJedis = redis.resource - private var subscriberThread: Thread? = null - private val isSubscribed = AtomicBoolean(false) - private val shouldReconnect = AtomicBoolean(true) - private val pubSub = createPubSub() - - init { - startSubscriber() - } - - private fun createPubSub(): JedisPubSub { - return object : JedisPubSub() { - override fun onPMessage(pattern: String, channel: String, message: String) { - if (!channel.startsWith("$namespace:")) return - - try { - val channelName = channel.substringAfter("$namespace:") - handleMessage(channelName, message) - } catch (e: Exception) { - logger.log(Level.WARNING, "Error processing message on channel $channel: ${e.message}") - } - } - } - } - - private fun handleMessage(channelName: String, message: String) { - try { - val payloadWrapper = message.asPayloadWrapper() - - when (payloadWrapper.state) { - PayloadWrapper.State.FORWARD -> handleForwardMessage(channelName, payloadWrapper) - PayloadWrapper.State.RESPOND -> handleResponseMessage(channelName, payloadWrapper) - PayloadWrapper.State.PROXY -> handleProxyMessage(channelName, payloadWrapper) - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Error parsing message: ${e.message}") - } - } - - private fun handleForwardMessage(channelName: String, payloadWrapper: PayloadWrapper) { - // Find matching publisher - Publisher.HANDLER_LIST - .filterIsInstance>() - .find { it.namespace == namespace && it.name == channelName } - ?.handleForwardMessage(payloadWrapper) - - // Find matching subscriber - Subscriber.HANDLER_LIST - .filterIsInstance>() - .find { it.namespace == namespace && it.name == channelName } - ?.handleForwardMessage(payloadWrapper) - } - - private fun handleResponseMessage(channelName: String, payloadWrapper: PayloadWrapper) { - Publisher.HANDLER_LIST - .filterIsInstance>() - .filter { it.namespace == namespace && it.name == channelName } - .forEach { it.handleResponseMessage(payloadWrapper) } - } - - private fun handleProxyMessage(channelName: String, payloadWrapper: PayloadWrapper) { - Subscriber.HANDLER_LIST - .filterIsInstance>() - .find { it.namespace == namespace && it.name == channelName } - ?.handleProxyMessage(payloadWrapper) - } - - private fun startSubscriber() { - if (!shouldReconnect.get() || isSubscribed.get()) return - - synchronized(this) { - if (isSubscribed.get()) return - - subscriberThread?.interrupt() - subscriberThread = Thread({ - while (shouldReconnect.get()) { - try { - subJedis = redis.resource - isSubscribed.set(true) - subJedis.psubscribe(pubSub, pattern) - } catch (e: JedisException) { - logger.log(Level.WARNING, "Redis connection lost for namespace $namespace: ${e.message}") - isSubscribed.set(false) - safeCloseJedis() - Thread.sleep(5000) - } catch (e: Exception) { - logger.log(Level.SEVERE, "Unexpected error in subscriber for namespace $namespace: ${e.message}") - isSubscribed.set(false) - safeCloseJedis() - Thread.sleep(5000) - } - } - }, "redis-namespace-sub-$namespace-${UUID.randomUUID().toString().split("-").first()}") - subscriberThread?.start() - } - } - - private fun safeCloseJedis() { - try { - subJedis.close() - } catch (e: Exception) { - logger.log(Level.WARNING, "Error closing Jedis connection for namespace $namespace: ${e.message}") - } - } - - fun shutdown() { - shouldReconnect.set(false) - pubSub.punsubscribe() - safeCloseJedis() - subscriberThread?.interrupt() - } - } -} diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisConnectionMeta.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisConnectionMeta.kt new file mode 100644 index 00000000..27e102f6 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisConnectionMeta.kt @@ -0,0 +1,11 @@ +package org.sayandev.stickynote.core.messaging.redis + +import kotlinx.coroutines.CoroutineDispatcher +import org.sayandev.stickynote.core.messaging.ConnectionMeta +import redis.clients.jedis.JedisPool + +data class RedisConnectionMeta( + val pool: JedisPool, + val dispatcher: CoroutineDispatcher, + override val timeoutMillis: Long = 5000L +): ConnectionMeta \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisPublisher.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisPublisher.kt new file mode 100644 index 00000000..db4c6067 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisPublisher.kt @@ -0,0 +1,102 @@ +package org.sayandev.stickynote.core.messaging.redis + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.Publisher +import org.sayandev.stickynote.core.utils.CoroutineUtils +import redis.clients.jedis.JedisPubSub +import redis.clients.jedis.exceptions.JedisConnectionException +import java.util.* +import java.util.logging.Logger + +open class RedisPublisher

( + messageMeta: MessageMeta, + connectionMeta: RedisConnectionMeta, + logger: Logger +) : Publisher( + messageMeta, + connectionMeta, + logger, +) { + + init { + val pubSub = object : JedisPubSub() { + override fun onMessage(channel: String, message: String) { + if (channel != this@RedisPublisher.messageMeta.id()) return + val result = message.asPayloadWrapper() + when (result.behaviour) { + PayloadBehaviour.FORWARD -> { + val wrappedPayload = message.asPayloadWrapper

() + if (wrappedPayload.excludeSource && isSource(wrappedPayload.uniqueId)) return + val payloadResult = handle(wrappedPayload.typedPayload(messageMeta.payloadType)) ?: return + connectionMeta.pool.resource.publish( + channel.toByteArray(), + PayloadWrapper( + wrappedPayload.uniqueId, + payloadResult, + PayloadBehaviour.RESPONSE, + wrappedPayload.source, + wrappedPayload.target, + wrappedPayload.excludeSource + ).asJson().toByteArray() + ) + } + PayloadBehaviour.RESPONSE -> { + for (publisher in HANDLER_LIST.filterIsInstance>()) { + if (publisher.messageMeta.id() == channel) { + publisher.payloads[result.uniqueId]?.apply { + this.complete(result.typedPayload(messageMeta.resultType)) + publisher.payloads.remove(result.uniqueId) + } + } + } + } + else -> { } + } + } + } + Thread({ + while (true) { + try { + connectionMeta.pool.resource.use { jedis -> + jedis.subscribe(pubSub, messageMeta.id()) + } + } catch (e: JedisConnectionException) { + logger.severe("Redis connection lost for channel ${messageMeta.id()}: ${e.message}. Retrying in ${connectionMeta.timeoutMillis} milliseconds...") + Thread.sleep(connectionMeta.timeoutMillis) + } catch (e: Exception) { + logger.severe("Unexpected error in RedisPublisher: ${e.message}") + Thread.sleep(connectionMeta.timeoutMillis) + } + } + }, "redis-pub-sub-thread-${messageMeta.id()}-${UUID.randomUUID().toString().split("-").first()}").start() + } + + override suspend fun publish(payload: PayloadWrapper

): CompletableDeferred { + if (!HANDLER_LIST.contains(this)) { + throw IllegalStateException("Publisher with id ${messageMeta.id()} is not registered") + } + + val result = super.publish(payload) + + CoroutineUtils.launch(connectionMeta.dispatcher) { + delay(connectionMeta.timeoutMillis) + if (result.isActive) { + result.completeExceptionally(IllegalStateException("Sent payload has not been responded in ${connectionMeta.timeoutMillis}ms. Payload: $payload (id: ${messageMeta.id()})")) + } + payloads.remove(payload.uniqueId) + } + + CoroutineUtils.launch(connectionMeta.dispatcher) { + connectionMeta.pool.resource.publish(messageMeta.id().toByteArray(), payload.asJson().toByteArray()) + } + + return result + } +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisSubscriber.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisSubscriber.kt new file mode 100644 index 00000000..fc31e017 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/redis/RedisSubscriber.kt @@ -0,0 +1,106 @@ +package org.sayandev.stickynote.core.messaging.redis + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.Publisher +import org.sayandev.stickynote.core.messaging.Subscriber +import org.sayandev.stickynote.core.utils.CoroutineUtils +import org.sayandev.stickynote.core.utils.CoroutineUtils.awaitWithTimeout +import redis.clients.jedis.JedisPubSub +import redis.clients.jedis.exceptions.JedisConnectionException +import java.util.* +import java.util.logging.Logger + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class RedisSubscriber

( + messageMeta: MessageMeta, + val connectionMeta: RedisConnectionMeta, + val logger: Logger +) : Subscriber(messageMeta) { + + init { + val pubSub = object : JedisPubSub() { + override fun onMessage(channel: String, message: String) { + if (channel != messageMeta.id()) return + val payloadWrapper = message.asPayloadWrapper

() + + when (payloadWrapper.behaviour) { + PayloadBehaviour.FORWARD_PROXY -> { + if (!isVelocity) return + + CoroutineUtils.launch(connectionMeta.dispatcher) { + val result = + (HANDLER_LIST.find { it.messageMeta.id() == messageMeta.id() } as Subscriber) + .onSubscribe(payloadWrapper.typedPayload(messageMeta.payloadType)) + result.await() + publishWithTimeout( + PayloadWrapper( + payloadWrapper.uniqueId, + result.getCompleted(), + PayloadBehaviour.RESPONSE, + payloadWrapper.source + ) + ) + } + } + PayloadBehaviour.FORWARD -> { + if (payloadWrapper.excludeSource && isSource(payloadWrapper.uniqueId)) return + CoroutineUtils.launch(connectionMeta.dispatcher) { + val result = + (HANDLER_LIST.find { it.messageMeta.id() == messageMeta.id() } as? Subscriber)?.onSubscribe( + payloadWrapper.typedPayload(messageMeta.payloadType) + ) + if (payloadWrapper.target == "PROCESSED") return@launch; + publishWithTimeout( + PayloadWrapper( + payloadWrapper.uniqueId, + result?.getCompleted() ?: payloadWrapper.payload, + if (result != null) PayloadBehaviour.RESPONSE else payloadWrapper.behaviour, + payloadWrapper.source, + "PROCESSED" + ) + ) + } + } + else -> { } + } + } + } + Thread({ + while (true) { + try { + connectionMeta.pool.resource.use { jedis -> + jedis.subscribe(pubSub, messageMeta.id()) + } + } catch (e: JedisConnectionException) { + logger.severe("Redis connection lost for channel ${messageMeta.id()}: ${e.message}. Retrying in ${connectionMeta.timeoutMillis} milliseconds...") + Thread.sleep(connectionMeta.timeoutMillis) + } catch (e: Exception) { + logger.severe("Unexpected error in RedisSubscriber: ${e.message}") + Thread.sleep(connectionMeta.timeoutMillis) + } + } + }, "redis-sub-sub-thread-${messageMeta.id()}-${UUID.randomUUID().toString().split("-").first()}").start() + } + + private suspend fun publishWithTimeout(payload: PayloadWrapper<*>) { + val deferred = CompletableDeferred() + CoroutineUtils.launch(connectionMeta.dispatcher) { + connectionMeta.pool.resource.publish(messageMeta.id().toByteArray(), payload.asJson().toByteArray()) + deferred.complete(Unit) + } + deferred.awaitWithTimeout(connectionMeta.timeoutMillis) { + logger.warning("failed to publish payload `${payload}` within ${connectionMeta.timeoutMillis}ms.") + } + } + + fun isSource(uniqueId: UUID): Boolean { + return Publisher.Companion.HANDLER_LIST.flatMap { publisher -> publisher.payloads.keys }.contains(uniqueId) + } +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/subscriber/RedisSubscriber.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/subscriber/RedisSubscriber.kt deleted file mode 100644 index 31326f8e..00000000 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/subscriber/RedisSubscriber.kt +++ /dev/null @@ -1,116 +0,0 @@ -package org.sayandev.stickynote.core.messaging.subscriber - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asJson -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.typedPayload -import org.sayandev.stickynote.core.messaging.publisher.Publisher -import org.sayandev.stickynote.core.messaging.redis.RedisConnectionManager -import org.sayandev.stickynote.core.utils.CoroutineUtils.launch -import redis.clients.jedis.JedisPool -import java.util.* -import java.util.logging.Level -import java.util.logging.Logger - -@OptIn(ExperimentalCoroutinesApi::class) -abstract class RedisSubscriber( - val dispatcher: CoroutineDispatcher, - val redis: JedisPool, - namespace: String, - name: String, - val payloadClass: Class

, - val logger: Logger -) : Subscriber(namespace, name) { - - val channel = "$namespace:$name" - - init { - RedisConnectionManager.registerNamespace(namespace, redis, dispatcher) - } - - fun handleForwardMessage(payloadWrapper: PayloadWrapper) { - if (payloadWrapper.excludeSource && isSource(payloadWrapper.uniqueId)) return - launch(dispatcher) { - try { - val typedPayload = payloadWrapper.typedPayload(payloadClass) - val result = onSubscribe(typedPayload) - if (result == null) return@launch - if (payloadWrapper.target == "PROCESSED") return@launch - - publish( - PayloadWrapper( - payloadWrapper.uniqueId, - result.getCompleted(), - PayloadWrapper.State.RESPOND, - payloadWrapper.source, - "PROCESSED" - ) - ) - } catch (e: Exception) { - logger.log(Level.WARNING, "Error handling forward message: ${e.message}") - } - } - } - - fun handleProxyMessage(payloadWrapper: PayloadWrapper) { - if (!isVelocity) return - - launch(dispatcher) { - try { - val typedPayload = payloadWrapper.typedPayload(payloadClass) - val result = onSubscribe(typedPayload) ?: return@launch - result.await() - publish( - PayloadWrapper( - payloadWrapper.uniqueId, - result.getCompleted(), - PayloadWrapper.State.RESPOND, - payloadWrapper.source - ) - ) - } catch (e: Exception) { - logger.log(Level.WARNING, "Error handling proxy message: ${e.message}") - } - } - } - - private suspend fun publish(payload: PayloadWrapper<*>) { - val publication = CompletableDeferred() - launch(dispatcher) { - delay(TIMEOUT_SECONDS * 1000) - if (publication.isActive) { - publication.completeExceptionally(IllegalStateException("Failed to publish payload in subscriber after $TIMEOUT_SECONDS seconds. Payload: $payload (channel: ${id()})")) - } - } - - val localJedis = redis.resource - try { - val published = localJedis.publish(channel.toByteArray(), payload.asJson().toByteArray()) - if (published <= 0) { - return - } - publication.complete(Unit) - } catch (e: Exception) { - logger.log(Level.WARNING, "Error publishing message: ${e.message}") - publication.completeExceptionally(e) - } finally { - localJedis.close() - } - } - - fun isSource(uniqueId: UUID): Boolean { - return Publisher.HANDLER_LIST - .asSequence() - .flatMap { publisher -> publisher.payloads.keys.asSequence() } - .contains(uniqueId) - } - - companion object { - val isVelocity = runCatching { Class.forName("com.velocitypowered.api.proxy.ProxyServer") != null }.isSuccess - - const val TIMEOUT_SECONDS = 5L - } -} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/subscriber/Subscriber.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/subscriber/Subscriber.kt deleted file mode 100644 index c2de16f4..00000000 --- a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/subscriber/Subscriber.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.sayandev.stickynote.core.messaging.subscriber - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred - -abstract class Subscriber( - val namespace: String, - val name: String, -) { - - abstract suspend fun onSubscribe(payload: P): CompletableDeferred? - - fun id(): String { - return "$namespace:$name" - } - - fun register() { - register(this) - } - - fun unregister() { - unregister(this) - } - - companion object { - val HANDLER_LIST = mutableListOf>() - - fun register(subscriber: Subscriber) { - HANDLER_LIST.add(subscriber) - } - - fun unregister(subscriber: Subscriber) { - HANDLER_LIST.remove(subscriber) - } - } - -} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/MessageWebSocketServer.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/MessageWebSocketServer.kt new file mode 100644 index 00000000..3baf3649 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/MessageWebSocketServer.kt @@ -0,0 +1,59 @@ +package org.sayandev.stickynote.core.messaging.websocket + +import org.java_websocket.WebSocket +import org.java_websocket.client.WebSocketClient +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.handshake.ServerHandshake +import org.java_websocket.server.WebSocketServer +import java.net.InetSocketAddress +import java.net.URI + +class MessageWebSocketServer( + uri: URI +) : WebSocketServer(InetSocketAddress(uri.host, uri.port)) { + override fun onOpen(connection: WebSocket, handshake: ClientHandshake) { + } + + override fun onClose(connection: WebSocket, code: Int, reason: String, remote: Boolean) { + // TODO: throw exception or something? + } + + override fun onMessage(connection: WebSocket, message: String) { + broadcast(message) + } + + override fun onError(connection: WebSocket, e: Exception) { + } + + override fun onStart() { + } + + companion object { + fun isWebSocketAvailable(uri: URI): Boolean { + var available = false + val client = object : WebSocketClient(uri) { + override fun onOpen(handshakedata: ServerHandshake?) { + available = true + close() + } + override fun onMessage(message: String?) {} + override fun onClose(code: Int, reason: String?, remote: Boolean) {} + override fun onError(ex: Exception?) {} + } + try { + client.connectBlocking() + } catch (e: Exception) { + return false + } + return available + } + + fun getWebSocketServer(uri: URI): MessageWebSocketServer? { + return if (isWebSocketAvailable(uri)) { + MessageWebSocketServer(uri) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketConnectionMeta.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketConnectionMeta.kt new file mode 100644 index 00000000..5b88a78c --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketConnectionMeta.kt @@ -0,0 +1,11 @@ +package org.sayandev.stickynote.core.messaging.websocket + +import kotlinx.coroutines.CoroutineDispatcher +import org.sayandev.stickynote.core.messaging.ConnectionMeta +import java.net.URI + +data class WebSocketConnectionMeta( + val uri: URI, + val dispatcher: CoroutineDispatcher, + override val timeoutMillis: Long = 5000L +): ConnectionMeta \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketPublisher.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketPublisher.kt new file mode 100644 index 00000000..4392df21 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketPublisher.kt @@ -0,0 +1,295 @@ +package org.sayandev.stickynote.core.messaging.websocket + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.java_websocket.WebSocket +import org.java_websocket.client.WebSocketClient +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.handshake.ServerHandshake +import org.java_websocket.server.WebSocketServer +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.Publisher +import org.sayandev.stickynote.core.utils.CoroutineUtils.launch +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import java.net.URI +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.logging.Logger +import kotlin.math.min + +open class WebSocketPublisher

( + messageMeta: MessageMeta, + connectionMeta: WebSocketConnectionMeta, + logger: Logger, +) : Publisher(messageMeta, connectionMeta, logger) { + + private var client: WebSocketClient? = null + private var server: WebSocketServer? = null + private val connectLock = Any() + @Volatile private var openSignal: CompletableDeferred? = null + + // Rate-limit connection error logs to avoid spam + private val logWindowMs = 3000L + @Volatile private var lastConnLog = 0L + + // Cooldown to avoid hammering connect attempts when the target is down + private val retryCooldownMs = 5000L + @Volatile private var nextRetryAt = 0L + + companion object { + // Single embedded server per port inside this JVM + private val SERVER_REGISTRY = ConcurrentHashMap() + + // Improved port check: try active connect; if no listener, try bind with SO_REUSEADDR. + private fun isPortFree(bindHost: String, port: Int): Boolean { + // If someone is listening, a connect will succeed -> not free + try { Socket("127.0.0.1", port).use { return false } } catch (_: Exception) { /* no listener */ } + return try { + ServerSocket().use { s -> + s.reuseAddress = true + s.bind(InetSocketAddress(bindHost, port)) + true + } + } catch (_: Exception) { + false + } + } + } + + init { + Runtime.getRuntime().addShutdownHook(Thread { closeQuietly() }) + connectOrStartServerBlocking() + } + + private fun infoRateLimited(msg: String) { + val now = System.currentTimeMillis() + if (now - lastConnLog >= logWindowMs) { + lastConnLog = now + logger.info(msg) + } + } + + private fun warnRateLimited(msg: String) { + val now = System.currentTimeMillis() + if (now - lastConnLog >= logWindowMs) { + lastConnLog = now + logger.warning(msg) + } + } + + private fun buildClient(uri: URI): WebSocketClient { + val signal = CompletableDeferred().also { openSignal = it } + return object : WebSocketClient(uri) { + override fun onOpen(handshakedata: ServerHandshake?) { +// logger.info("Connected to WebSocket server ${uri.host}:${uri.port}") + if (!signal.isCompleted) signal.complete(Unit) + this.setConnectionLostTimeout(10) + } + override fun onMessage(message: String?) { + if (message != null) handleMessage(message) + } + override fun onClose(code: Int, reason: String?, remote: Boolean) { + // Rate-limited to avoid spam during reconnect +// infoRateLimited("WebSocket closed (${uri.host}:${uri.port}): $reason") + } + override fun onError(ex: Exception) { + // Rate-limited to avoid spam during reconnect +// warnRateLimited("WebSocket error (${uri.host}:${uri.port}): ${ex.message}") + } + } + } + + // Start an embedded server locally; reuse if already running; enable SO_REUSEADDR. + private fun startServer(bindHost: String, port: Int) { + SERVER_REGISTRY[port]?.let { + server = it + logger.info("Reusing existing WS server on $bindHost:$port") + return + } + if (!isPortFree(bindHost, port)) { + logger.info("WS server port $port is busy; skipping embedded server start.") + return + } + val srv = object : WebSocketServer(InetSocketAddress(bindHost, port)) { + override fun onOpen(conn: WebSocket, handshake: ClientHandshake?) { + logger.info("WS client connected: ${conn.remoteSocketAddress}") + } + override fun onClose(conn: WebSocket, code: Int, reason: String?, remote: Boolean) { + logger.info("WS client disconnected: ${conn.remoteSocketAddress}") + } + override fun onMessage(conn: WebSocket, message: String?) { + if (message != null) handleMessage(message) + } + override fun onError(conn: WebSocket?, ex: Exception) { + logger.warning("WS server error: ${ex.message}") + } + override fun onStart() { + logger.info("WS server started on $address") + } + }.apply { + @Suppress("DEPRECATION") + this.isReuseAddr = true + } + try { + srv.start() + SERVER_REGISTRY[port] = srv + server = srv + } catch (e: Exception) { + logger.warning("Failed to start WS server on $bindHost:$port: ${e.message}") + } + } + + private fun closeQuietly() { + try { + client?.let { + try { if (it.isOpen || it.isClosing) it.closeBlocking() else it.close() } catch (_: Exception) {} + } + } finally { client = null } + try { + server?.let { + try { it.stop(1000) } catch (_: Exception) {} + SERVER_REGISTRY.entries.removeIf { e -> e.value === server } + } + } finally { server = null } + } + + private fun replaceClient(newUri: URI): Boolean { + client?.let { + try { if (it.isOpen || it.isClosing) it.closeBlocking() else it.close() } catch (_: Exception) {} + } + val c = buildClient(newUri) + client = c + return try { + c.connectBlocking(1500, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + infoRateLimited("Connection failed ($newUri): ${e.message}") + false + } + } + + // Retry connect with exponential backoff; optionally wait for onOpen. + private fun connectWithRetry(uri: URI, maxAttempts: Int = 6, maxBackoffMs: Long = 2000): Boolean { + logger.info("Trying to open a new websocket connection on ${uri.host}:${uri.port}") + var attempt = 0 + while (attempt < maxAttempts) { + attempt++ + if (replaceClient(uri)) { + val ok = runCatching { + runBlocking { + withTimeoutOrNull(500) { openSignal?.await() } != null && (client?.isOpen == true) + } + }.getOrDefault(false) + if (ok) return true + } + val delayMs = min(150L * (1 shl (attempt - 1)), maxBackoffMs) + try { Thread.sleep(delayMs) } catch (_: InterruptedException) {} + } + return false + } + + // Single-flight connect: apply cooldown after a full failure to avoid log spam. + private fun connectOrStartServerBlocking(): Boolean { + val now = System.currentTimeMillis() + if (now < nextRetryAt) return false + + val targetUri = connectionMeta.uri + synchronized(connectLock) { + client?.let { if (it.isOpen) { nextRetryAt = 0; return true } } + + // 1) Connect to target with retry/backoff + if (connectWithRetry(targetUri)) { nextRetryAt = 0; return true } + + // 2) Start embedded server if possible; then connect to local fallback + startServer("0.0.0.0", targetUri.port) + try { Thread.sleep(200) } catch (_: InterruptedException) {} + val localFallback = URI("${targetUri.scheme}://127.0.0.1:${targetUri.port}${targetUri.rawPath ?: ""}") + val ok = connectWithRetry(localFallback) + nextRetryAt = if (ok) 0 else System.currentTimeMillis() + retryCooldownMs + return ok + } + } + + private fun handleMessage(message: String) { + val result = message.asPayloadWrapper() + when (result.behaviour) { + PayloadBehaviour.FORWARD -> { + val wrappedPayload = message.asPayloadWrapper

() + if (wrappedPayload.excludeSource && isSource(wrappedPayload.uniqueId)) return + val payloadResult = handle(wrappedPayload.typedPayload(messageMeta.payloadType)) ?: return + client?.takeIf { it.isOpen }?.send( + PayloadWrapper( + wrappedPayload.uniqueId, + payloadResult, + PayloadBehaviour.RESPONSE, + wrappedPayload.source, + wrappedPayload.target, + wrappedPayload.excludeSource + ).asJson() + ) + } + PayloadBehaviour.RESPONSE -> { + for (publisher in HANDLER_LIST.filterIsInstance>()) { + if (publisher.messageMeta.id() == messageMeta.id()) { + publisher.payloads[result.uniqueId]?.apply { + this.complete(result.typedPayload(messageMeta.resultType)) + publisher.payloads.remove(result.uniqueId) + } + } + } + } + PayloadBehaviour.FORWARD_PROXY -> {} + } + } + + override suspend fun publish(payload: PayloadWrapper

): CompletableDeferred { + if (!HANDLER_LIST.contains(this)) { + throw IllegalStateException("Publisher with id ${messageMeta.id()} is not registered") + } + + val result = super.publish(payload) + + // Timeout guard for response + launch(this@WebSocketPublisher.connectionMeta.dispatcher) { + delay(connectionMeta.timeoutMillis) + if (result.isActive) { + result.completeExceptionally( + IllegalStateException( + "Sent payload has not been responded in ${connectionMeta.timeoutMillis}ms. Payload: $payload (id: ${messageMeta.id()})" + ) + ) + } + payloads.remove(payload.uniqueId) + } + + // Safe send with connect/retry and cooldown + launch(this@WebSocketPublisher.connectionMeta.dispatcher) { + val connected = connectOrStartServerBlocking() + val ws = client + if (!connected || ws == null || !ws.isOpen) { + result.completeExceptionally( + IllegalStateException("WebSocket is not connected to ${connectionMeta.uri}. Payload not sent.") + ) + payloads.remove(payload.uniqueId) + return@launch + } + try { + withTimeoutOrNull(500) { openSignal?.await() } + ws.send(payload.asJson()) + } catch (e: Exception) { + result.completeExceptionally(e) + payloads.remove(payload.uniqueId) + } + } + + return result + } +} \ No newline at end of file diff --git a/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketSubscriber.kt b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketSubscriber.kt new file mode 100644 index 00000000..20ad3b91 --- /dev/null +++ b/stickynote-core/src/main/kotlin/org/sayandev/stickynote/core/messaging/websocket/WebSocketSubscriber.kt @@ -0,0 +1,111 @@ +package org.sayandev.stickynote.core.messaging.websocket + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.java_websocket.client.WebSocketClient +import org.java_websocket.handshake.ServerHandshake +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.Publisher +import org.sayandev.stickynote.core.messaging.Subscriber +import org.sayandev.stickynote.core.utils.CoroutineUtils +import org.sayandev.stickynote.core.utils.CoroutineUtils.awaitWithTimeout +import java.util.UUID +import java.util.logging.Logger + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class WebSocketSubscriber

( + messageMeta: MessageMeta, + val connectionMeta: WebSocketConnectionMeta, + val logger: Logger +) : Subscriber(messageMeta) { + + private val client: WebSocketClient + + init { + client = object : WebSocketClient(connectionMeta.uri) { + override fun onOpen(handshakedata: ServerHandshake?) { + } + + override fun onMessage(message: String) { + handleMessage(message) + } + + override fun onClose(code: Int, reason: String?, remote: Boolean) { + logger.info("WebSocket connection closed: $reason") + } + + override fun onError(ex: Exception?) { + logger.severe("WebSocket error: ${ex?.message}") + } + } + client.connect() + } + + private fun handleMessage(message: String) { + val payloadWrapper = message.asPayloadWrapper

() + + when (payloadWrapper.behaviour) { + PayloadBehaviour.FORWARD_PROXY -> { + if (!isVelocity) return + + CoroutineUtils.launch(connectionMeta.dispatcher) { + val result = (HANDLER_LIST.find { it.messageMeta.id() == messageMeta.id() } as Subscriber) + .onSubscribe(payloadWrapper.typedPayload(messageMeta.payloadType)) + result.await() + publishWithTimeout( + PayloadWrapper( + payloadWrapper.uniqueId, + result.getCompleted(), + PayloadBehaviour.RESPONSE, + payloadWrapper.source + ) + ) + } + } + PayloadBehaviour.FORWARD -> { + if (payloadWrapper.excludeSource && isSource(payloadWrapper.uniqueId)) return + CoroutineUtils.launch(connectionMeta.dispatcher) { + val result = + (HANDLER_LIST.find { it.messageMeta.id() == messageMeta.id() } as? Subscriber)?.onSubscribe( + payloadWrapper.typedPayload(messageMeta.payloadType) + ) + if (payloadWrapper.target == "PROCESSED") return@launch + publishWithTimeout( + PayloadWrapper( + payloadWrapper.uniqueId, + result?.getCompleted() ?: payloadWrapper.payload, + if (result != null) PayloadBehaviour.RESPONSE else payloadWrapper.behaviour, + payloadWrapper.source, + "PROCESSED" + ) + ) + } + } + else -> { } + } + } + + private suspend fun publishWithTimeout(payload: PayloadWrapper<*>) { + val deferred = CompletableDeferred() + CoroutineUtils.launch(connectionMeta.dispatcher) { + client.send(payload.asJson()) + deferred.complete(Unit) + } + deferred.awaitWithTimeout(TIMEOUT_SECONDS * 1000L) { + logger.warning("failed to publish payload `${payload}` within $TIMEOUT_SECONDS seconds.") + } + } + + fun isSource(uniqueId: UUID): Boolean { + return Publisher.Companion.HANDLER_LIST.flatMap { publisher -> publisher.payloads.keys }.contains(uniqueId) + } + + companion object { + const val TIMEOUT_SECONDS = 5L + } +} \ No newline at end of file diff --git a/stickynote-loader/src/main/kotlin/org/sayandev/plugin/StickyNoteProjectPlugin.kt b/stickynote-loader/src/main/kotlin/org/sayandev/plugin/StickyNoteProjectPlugin.kt index cad0f17e..cce95ac9 100644 --- a/stickynote-loader/src/main/kotlin/org/sayandev/plugin/StickyNoteProjectPlugin.kt +++ b/stickynote-loader/src/main/kotlin/org/sayandev/plugin/StickyNoteProjectPlugin.kt @@ -50,6 +50,16 @@ class StickyNoteProjectPlugin : Plugin { target.dependencies.extensions.create("stickynote", StickyLoadDependencyExtension::class.java, target) + /*target.buildscript { + repositories { mavenCentral() } + + dependencies { + val kotlinVersion = "2.2.0" + classpath(kotlin("gradle-plugin", version = kotlinVersion)) + classpath(kotlin("serialization", version = kotlinVersion)) + } + }*/ + target.plugins.apply("com.gradleup.shadow") target.plugins.apply("org.jetbrains.kotlin.jvm") target.plugins.apply("java-library") @@ -178,8 +188,8 @@ class StickyNoteProjectPlugin : Plugin { project.dependencies.add("compileOnlyApi", "org.sayandev:stickynote-core:${createStickyNoteLoader.loaderVersion.get()}") project.dependencies.add("testImplementation", "org.sayandev:stickynote-core:${createStickyNoteLoader.loaderVersion.get()}") - project.dependencies.add("compileOnlyApi", "org.jetbrains.kotlin:kotlin-stdlib:${KotlinVersion.CURRENT}") - project.dependencies.add("testImplementation", "org.jetbrains.kotlin:kotlin-stdlib:${KotlinVersion.CURRENT}") +// project.dependencies.add("compileOnlyApi", "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") +// project.dependencies.add("testImplementation", "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") if (config.modules.get().map { it.type }.contains(StickyNoteModules.BUKKIT)) { project.dependencies.add("implementation", "org.sayandev:stickynote-loader-bukkit:${createStickyNoteLoader.loaderVersion.get()}") diff --git a/stickynote-loader/stickynote-loader-common/src/main/java/org/sayandev/loader/common/StickyNoteLoader.java b/stickynote-loader/stickynote-loader-common/src/main/java/org/sayandev/loader/common/StickyNoteLoader.java index 69361745..9263b78c 100644 --- a/stickynote-loader/stickynote-loader-common/src/main/java/org/sayandev/loader/common/StickyNoteLoader.java +++ b/stickynote-loader/stickynote-loader-common/src/main/java/org/sayandev/loader/common/StickyNoteLoader.java @@ -44,6 +44,7 @@ public void load(String id, File dataDirectory, Logger logger, LibraryManager li try { List generatedDependencies = new ArrayList<>(getDependencies(stickyNotes)); +// generatedDependencies.add(new Dependency("org{}jetbrains{}kotlin", "kotlin-stdlib", "2.1.0", null, false)); List repositories = getRepositories(stickyNotes); Object relocation = stickyNotes.getField("RELOCATION").get(stickyNotes); diff --git a/stickynote-proxy/build.gradle.kts b/stickynote-proxy/build.gradle.kts index eb144fcd..16dac791 100644 --- a/stickynote-proxy/build.gradle.kts +++ b/stickynote-proxy/build.gradle.kts @@ -1,5 +1,5 @@ dependencies { compileOnlyApi(project(":stickynote-core")) - api(libs.kotlin) +// api(libs.kotlin) } \ No newline at end of file diff --git a/stickynote-proxy/stickynote-proxy-bungeecord/build.gradle.kts b/stickynote-proxy/stickynote-proxy-bungeecord/build.gradle.kts index 0dc38e67..bc09e1be 100644 --- a/stickynote-proxy/stickynote-proxy-bungeecord/build.gradle.kts +++ b/stickynote-proxy/stickynote-proxy-bungeecord/build.gradle.kts @@ -2,9 +2,9 @@ dependencies { compileOnly(libs.bungeecord) api(libs.mccoroutines.bungeecord.api) api(libs.mccoroutines.bungeecord.core) + api(libs.adventure.platform.bungeecord) compileOnlyApi(libs.libby.bungee) - compileOnlyApi(libs.sayanventure.platform.bungeecord) compileOnly(project(":stickynote-core")) compileOnly(project(":stickynote-proxy")) diff --git a/stickynote-proxy/stickynote-proxy-bungeecord/src/main/kotlin/org/sayandev/stickynote/bungeecord/utils/AdventureUtils.kt b/stickynote-proxy/stickynote-proxy-bungeecord/src/main/kotlin/org/sayandev/stickynote/bungeecord/utils/AdventureUtils.kt index ba4b12fa..607459e7 100644 --- a/stickynote-proxy/stickynote-proxy-bungeecord/src/main/kotlin/org/sayandev/stickynote/bungeecord/utils/AdventureUtils.kt +++ b/stickynote-proxy/stickynote-proxy-bungeecord/src/main/kotlin/org/sayandev/stickynote/bungeecord/utils/AdventureUtils.kt @@ -2,10 +2,10 @@ package org.sayandev.stickynote.bungeecord.utils import net.md_5.bungee.api.CommandSender import net.md_5.bungee.api.connection.ProxiedPlayer -import org.sayandev.sayanventure.adventure.platform.bungeecord.BungeeAudiences -import org.sayandev.sayanventure.adventure.text.Component -import org.sayandev.sayanventure.adventure.text.minimessage.MiniMessage -import org.sayandev.sayanventure.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver import org.sayandev.stickynote.bungeecord.plugin object AdventureUtils { diff --git a/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessagePublisher.kt b/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessagePublisher.kt index 36f735e7..17bc6baf 100644 --- a/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessagePublisher.kt +++ b/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessagePublisher.kt @@ -6,26 +6,29 @@ import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier import com.velocitypowered.api.proxy.server.RegisteredServer import kotlinx.coroutines.CompletableDeferred -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asJson -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asPayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.typedPayload -import org.sayandev.stickynote.core.messaging.publisher.Publisher -import org.sayandev.stickynote.velocity.StickyNote +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.SimpleConnectionMeta +import org.sayandev.stickynote.core.messaging.Publisher import org.sayandev.stickynote.velocity.registerListener import org.sayandev.stickynote.velocity.server +import java.util.logging.Logger -abstract class PluginMessagePublisher( - namespace: String, - name: String, - val resultClass: Class -): Publisher( - StickyNote.javaLogger, - namespace, - name +abstract class PluginMessagePublisher

( + messageMeta: MessageMeta, + connectionMeta: SimpleConnectionMeta, + logger: Logger +): Publisher( + messageMeta, + connectionMeta, + logger ) { - val channelIdentifier: MinecraftChannelIdentifier = MinecraftChannelIdentifier.create(namespace, name) + val channelIdentifier: MinecraftChannelIdentifier = MinecraftChannelIdentifier.create(messageMeta.namespace, messageMeta.name) init { registerChannel() @@ -36,39 +39,39 @@ abstract class PluginMessagePublisher( registerListener(this) } - suspend fun publish(server: RegisteredServer, payloadWrapper: PayloadWrapper

): CompletableDeferred { + suspend fun publish(server: RegisteredServer, payloadWrapper: PayloadWrapper

): CompletableDeferred { server.sendPluginMessage(channelIdentifier, payloadWrapper.asJson().toByteArray()) return publish(payloadWrapper) } - suspend fun publish(player: Player, payloadWrapper: PayloadWrapper

): CompletableDeferred { + suspend fun publish(player: Player, payloadWrapper: PayloadWrapper

): CompletableDeferred { player.sendPluginMessage(channelIdentifier, payloadWrapper.asJson().toByteArray()) return publish(payloadWrapper) } - abstract fun handle(payload: P): S? + abstract override fun handle(payload: P): R? @Subscribe fun onMessageReceived(event: PluginMessageEvent) { val data = event.data val channel = event.identifier.id if (channel != channelIdentifier.id) return - val result = String(data, Charsets.UTF_8).asPayloadWrapper() - when (result.state) { - PayloadWrapper.State.RESPOND -> { - for (publisher in HANDLER_LIST.filterIsInstance>()) { - if (publisher.id() == channel) { + val result = String(data, Charsets.UTF_8).asPayloadWrapper() + when (result.behaviour) { + PayloadBehaviour.RESPONSE -> { + for (publisher in HANDLER_LIST.filterIsInstance>()) { + if (messageMeta.id() == channel) { publisher.payloads[result.uniqueId]?.apply { - this.complete(result.typedPayload(resultClass)) + this.complete(result.typedPayload(messageMeta.resultType)) publisher.payloads.remove(result.uniqueId) } ?: throw IllegalStateException("No payload found for uniqueId ${result.uniqueId}") // throws exception if the payload doesn't belong to this server } } } - PayloadWrapper.State.PROXY -> {} + PayloadBehaviour.FORWARD_PROXY -> { } else -> { - throw IllegalStateException("a result payload has been received with ${result.state} state, but it doesn't belong here. (payload: ${result})") + throw IllegalStateException("a result payload has been received with ${result.behaviour} state, but it doesn't belong here. (payload: ${result})") } } } -} \ No newline at end of file +} diff --git a/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessageSubscribeListener.kt b/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessageSubscribeListener.kt index 80e561f3..ce1ab4e9 100644 --- a/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessageSubscribeListener.kt +++ b/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/PluginMessageSubscribeListener.kt @@ -5,21 +5,21 @@ import com.velocitypowered.api.event.connection.PluginMessageEvent import com.velocitypowered.api.proxy.ServerConnection import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asJson -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.asPayloadWrapper -import org.sayandev.stickynote.core.messaging.publisher.PayloadWrapper.Companion.typedPayload -import org.sayandev.stickynote.core.messaging.subscriber.Subscriber +import org.sayandev.stickynote.core.messaging.MessageMeta +import org.sayandev.stickynote.core.messaging.PayloadBehaviour +import org.sayandev.stickynote.core.messaging.PayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asJson +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.asPayloadWrapper +import org.sayandev.stickynote.core.messaging.PayloadWrapper.Companion.typedPayload +import org.sayandev.stickynote.core.messaging.Subscriber import org.sayandev.stickynote.velocity.* import java.nio.charset.StandardCharsets -class PluginMessageSubscribeListener( - namespace: String, - name: String, - val payloadClass: Class

+class PluginMessageSubscribeListener

( + val messageMeta: MessageMeta ) { - val channelIdentifier = MinecraftChannelIdentifier.create(namespace, name) + val channelIdentifier = MinecraftChannelIdentifier.create(messageMeta.namespace, messageMeta.name) init { server.channelRegistrar.register(channelIdentifier) @@ -36,47 +36,47 @@ class PluginMessageSubscribeListener( val rawMessage = String(event.data, StandardCharsets.UTF_8) val payloadWrapper = rawMessage.asPayloadWrapper

() - when (payloadWrapper.state) { - PayloadWrapper.State.PROXY -> { + when (payloadWrapper.behaviour) { + PayloadBehaviour.FORWARD_PROXY -> { launch { - val result = (Subscriber.HANDLER_LIST.find { it.namespace == channelIdentifier.namespace && it.name == channelIdentifier.name } as Subscriber).onSubscribe(payloadWrapper.typedPayload(payloadClass)) - result?.invokeOnCompletion { - (event.source as ServerConnection).sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, result.getCompleted(), PayloadWrapper.State.RESPOND, payloadWrapper.source).asJson().toByteArray()) + val result = (Subscriber.HANDLER_LIST.find { it.messageMeta.namespace == channelIdentifier.namespace && it.messageMeta.name == channelIdentifier.name } as Subscriber).onSubscribe(payloadWrapper.typedPayload(messageMeta.payloadType)) + result.invokeOnCompletion { + (event.source as ServerConnection).sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, result.getCompleted(), PayloadBehaviour.RESPONSE, payloadWrapper.source).asJson().toByteArray()) } } } - PayloadWrapper.State.FORWARD -> { + PayloadBehaviour.FORWARD -> { launch { val targetServerName = payloadWrapper.target val targetServer = StickyNote.server.allServers.find { it.serverInfo.name == targetServerName } - val result = (Subscriber.HANDLER_LIST.find { it.namespace == channelIdentifier.namespace && it.name == channelIdentifier.name } as? Subscriber)?.onSubscribe(payloadWrapper.typedPayload(payloadClass)) + val result = (Subscriber.HANDLER_LIST.find { it.messageMeta.namespace == channelIdentifier.namespace && it.messageMeta.name == channelIdentifier.name } as? Subscriber)?.onSubscribe(payloadWrapper.typedPayload(messageMeta.payloadType)) result?.invokeOnCompletion { if (targetServerName != null) { - targetServer?.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, result.getCompleted(), payloadWrapper.state, source.serverInfo.name, targetServerName).asJson().toByteArray()) ?: warn("target server name was specified as ${targetServerName} but there's not server with this id. will ignore pluginmessage request") + targetServer?.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, result.getCompleted(), payloadWrapper.behaviour, source.serverInfo.name, targetServerName).asJson().toByteArray()) ?: warn("target server name was specified as ${targetServerName} but there's not server with this id. will ignore pluginmessage request") } else { for (server in StickyNote.server.allServers) { if (payloadWrapper.excludeSource && source.serverInfo.name == server.serverInfo.name) continue - server.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, result.getCompleted(), payloadWrapper.state, source.serverInfo.name, server.serverInfo.name).asJson().toByteArray()) + server.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, result.getCompleted(), payloadWrapper.behaviour, source.serverInfo.name, server.serverInfo.name).asJson().toByteArray()) } } } ?: let { if (targetServerName != null) { - targetServer?.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, payloadWrapper.payload, payloadWrapper.state, source.serverInfo.name, targetServerName).asJson().toByteArray()) ?: warn("target server name was specified as ${targetServerName} but there's not server with this id. will ignore pluginmessage request") + targetServer?.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, payloadWrapper.payload, payloadWrapper.behaviour, source.serverInfo.name, targetServerName).asJson().toByteArray()) ?: warn("target server name was specified as ${targetServerName} but there's not server with this id. will ignore pluginmessage request") } else { for (server in StickyNote.server.allServers) { if (payloadWrapper.excludeSource && source.serverInfo.name == server.serverInfo.name) continue - server.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, payloadWrapper.payload, payloadWrapper.state, source.serverInfo.name, server.serverInfo.name).asJson().toByteArray()) + server.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, payloadWrapper.payload, payloadWrapper.behaviour, source.serverInfo.name, server.serverInfo.name).asJson().toByteArray()) } } } } } - PayloadWrapper.State.RESPOND -> { + PayloadBehaviour.RESPONSE -> { val payloadSource = payloadWrapper.source ?: throw IllegalArgumentException("Can't respond a message if the source is null (payload: ${payloadWrapper})") val sourceServer = StickyNote.server.allServers.find { it.serverInfo.name.lowercase() == payloadSource.lowercase() } ?: throw IllegalArgumentException("Can't find the source server on proxy (payload: ${payloadWrapper})") - sourceServer.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, payloadWrapper.payload, payloadWrapper.state, payloadSource, payloadWrapper.target).asJson().toByteArray()) + sourceServer.sendPluginMessage(channelIdentifier, PayloadWrapper(payloadWrapper.uniqueId, payloadWrapper.payload, payloadWrapper.behaviour, payloadSource, payloadWrapper.target).asJson().toByteArray()) } } } -} \ No newline at end of file +} diff --git a/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/ProxySubscriber.kt b/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/ProxySubscriber.kt deleted file mode 100644 index ab554790..00000000 --- a/stickynote-proxy/stickynote-proxy-velocity/src/main/kotlin/org/sayandev/stickynote/velocity/messaging/ProxySubscriber.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.sayandev.stickynote.velocity.messaging - -import org.sayandev.stickynote.core.messaging.subscriber.Subscriber - -abstract class ProxySubscriber(namespace: String, name: String, payloadClass: Class

) : Subscriber(namespace, name) { - init { - PluginMessageSubscribeListener(namespace, name, payloadClass) - } -} \ No newline at end of file