("commands")
+
+ suspend fun start() {
+ logger.info("SimpleCloud v3 connection plugin initialized!")
+ config.save("config", connectionConfig)
+ config.save("messages", messageConfig)
+ config.save("commands", commandConfig)
+ startRegistration()
+ }
+
+ fun shutdown() {
+ logger.info("SimpleCloud v3 connection plugin uninitialized!")
+ config.close()
+ if (connectionConfig.get().registration.enabled) {
+ listener.stop()
+ }
+ }
+
+ private suspend fun startRegistration() {
+ if (connectionConfig.get().registration.enabled) {
+ loadExistingServers()
+ listener.start()
+ }
+ }
+
+ private suspend fun loadExistingServers() {
+ val servers = api.server().getAllServers(
+ ServerQuery.create()
+ .filterByState(ServerState.AVAILABLE)
+ .filterByServerGroupType(GroupServerType.SERVER)
+ ).await()
+
+ logger.info("Found ${servers.size} servers")
+ servers.forEach { listener.register(it) }
+ }
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/PermissionChecker.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/PermissionChecker.kt
deleted file mode 100644
index 77827e1..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/PermissionChecker.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package app.simplecloud.plugin.connection.shared
-
-fun interface PermissionChecker {
-
- fun checkPermission(player: P, permission: String): Boolean
-
-}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ServerConnectionPlugin.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ServerConnectionPlugin.kt
deleted file mode 100644
index 5d87c1c..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ServerConnectionPlugin.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package app.simplecloud.plugin.connection.shared
-
-import app.simplecloud.plugin.connection.shared.config.CommandConfig
-import app.simplecloud.plugin.connection.shared.config.ConfigFactory
-import app.simplecloud.plugin.connection.shared.config.ConnectionConfig
-import app.simplecloud.plugin.connection.shared.config.TargetConnectionConfig
-import app.simplecloud.plugin.connection.shared.server.ServerConnectionInfoGetter
-import java.nio.file.Path
-
-class ServerConnectionPlugin
(
- private val dataDirectory: Path,
- private val serverConnectionInfoGetter: ServerConnectionInfoGetter,
- private val permissionChecker: PermissionChecker
-) {
-
- val config = ConfigFactory.loadOrCreate(dataDirectory)
-
- fun getCommandConfigs(): List {
- return config.commands
- }
-
- fun getServerNameForLogin(player: P): String? {
- return getConnectionAndNameForLogin(player)?.second
- }
-
- fun getConnectionAndNameForLogin(player: P): Pair? {
- return getConnectionAndName(player, config.networkJoinTargets.targetConnections)
- }
-
- fun getConnectionAndNameForFallback(player: P, fromServerName: String): Pair? {
- return getConnectionAndName(player, config.fallbackConnectionsConfig.targetConnections, fromServerName)
- }
-
- fun getConnectionAndNameForCommand(player: P, commandConfig: CommandConfig): Pair? {
- return getConnectionAndName(player, commandConfig.targetConnections)
- }
-
- private fun getConnectionAndName(player: P, targetConnections: List, fromServerName: String = ""): Pair? {
- val possibleConnections = getPossibleServerConnections(player)
- val filteredTargetConnections = targetConnections.asSequence()
- .filter { fromServerName.isBlank() || matchesTargetConnection(it, fromServerName) }
- val possibleConnectionsWithTarget = possibleConnections.asSequence().mapNotNull { possibleConnection ->
- val targetConfig = filteredTargetConnections
- .firstOrNull { possibleConnection.name == it.name } ?: return@mapNotNull null
- ConnectionAndTargetConfig(possibleConnection, targetConfig)
- }
-
- val connectionAndTargetConfig = possibleConnectionsWithTarget.maxByOrNull { it.targetConfig.priority }?: return null
- val bestServerToConnect = getBestServerToConnect(fromServerName, connectionAndTargetConfig.connectionConfig)?: return null
- return Pair(connectionAndTargetConfig, bestServerToConnect)
- }
-
- private fun matchesTargetConnection(
- targetConnectionConfig: TargetConnectionConfig,
- fromServerName: String
- ): Boolean {
- if (targetConnectionConfig.from.isEmpty())
- return true
- return targetConnectionConfig.from.any { it.matches(fromServerName) }
- }
-
- private fun getPossibleServerConnections(
- player: P
- ): List {
- val serverConnectionInfos = serverConnectionInfoGetter.get()
- val serverNames = serverConnectionInfos.map { it.name }
-
- return config.connections.filter { connection ->
- connection.serverNameMatcher.anyMatches(serverNames)
- && connection.rules.all { it.isAllowed(player, permissionChecker) }
- }
- }
-
- private fun getBestServerToConnect(fromServerName: String, bestConnection: ConnectionConfig): String? {
- val serverConnectionInfos = serverConnectionInfoGetter.get()
- val bestServer = serverConnectionInfos
- .filter { it.name != fromServerName }
- .sortedBy { it.onlinePlayers }
- .firstOrNull { bestConnection.serverNameMatcher.matches(it.name) }
- return bestServer?.name
- }
-
-}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/CommandConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/CommandConfig.kt
index ad980b6..c721cef 100644
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/CommandConfig.kt
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/CommandConfig.kt
@@ -1,13 +1,26 @@
package app.simplecloud.plugin.connection.shared.config
+import app.simplecloud.plugin.connection.shared.utilities.ConfigVersion
+import app.simplecloud.plugin.connection.shared.utilities.DefaultConfigs
import org.spongepowered.configurate.objectmapping.ConfigSerializable
@ConfigSerializable
data class CommandConfig(
+ val version: Char = ConfigVersion.VERSION,
+ val commands: List = DefaultConfigs.COMMANDS,
+)
+
+@ConfigSerializable
+data class CommandEntry(
val name: String = "",
- val aliases: List = emptyList(),
- val targetConnections: List = emptyList(),
- val alreadyConnectedMessage: String = "You are already connected to this group!",
- val noTargetConnectionFound: String = "Couldn't find a target connection!",
+ val aliases: List = listOf(),
+ val targetConnections: List = listOf(),
+ val messages: CommandMessages = CommandMessages(),
val permission: String = "",
)
+
+@ConfigSerializable
+data class CommandMessages(
+ val alreadyConnected: String = "You are already connected to this server!",
+ val noTargetConnectionFound: String = "Couldn't find a target server!",
+)
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/Config.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/Config.kt
deleted file mode 100644
index 8e325eb..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/Config.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package app.simplecloud.plugin.connection.shared.config
-
-import app.simplecloud.plugin.api.shared.matcher.MatcherType
-import app.simplecloud.plugin.api.shared.matcher.ServerMatcherConfiguration
-import org.spongepowered.configurate.objectmapping.ConfigSerializable
-@ConfigSerializable
-data class Config(
- val version: String = "1",
- val connections: List = emptyList(),
- val networkJoinTargets: TargetsConfig = TargetsConfig(
- noTargetConnectionFoundMessage = "Couldn't connect you to the network because no target servers are available."
- ),
- val fallbackConnectionsConfig: TargetsConfig = TargetsConfig(
- noTargetConnectionFoundMessage = "You have been disconnected from the network since you have been kicked and no fallback server are available."
- ),
- val commands: List = emptyList(),
-) {
- companion object {
- fun createDefaultConfig(): Config {
- val defaultConnections = listOf(
- ConnectionConfig(
- name = "lobby",
- serverNameMatcher = ServerMatcherConfiguration(
- operation = MatcherType.STARTS_WITH,
- value = "lobby"
- )
- ),
- ConnectionConfig(
- name = "hub",
- serverNameMatcher = ServerMatcherConfiguration(
- operation = MatcherType.STARTS_WITH,
- value = "hub"
- )
- ),
- ConnectionConfig(
- name = "premium-lobby",
- serverNameMatcher = ServerMatcherConfiguration(
- operation = MatcherType.STARTS_WITH,
- value = "premium"
- ),
- rules = listOf(
- RulesConfig(
- type = RulesConfig.Type.PERMISSION,
- name = "simplecloud.connection.premium",
- value = "true",
- )
- )
- ),
- ConnectionConfig(
- name = "vip-lobby",
- serverNameMatcher = ServerMatcherConfiguration(
- operation = MatcherType.STARTS_WITH,
- value = "vip"
- ),
- rules = listOf(
- RulesConfig(
- type = RulesConfig.Type.PERMISSION,
- name = "simplecloud.connection.vip",
- value = "true",
- )
- )
- ),
- ConnectionConfig(
- name = "silent-lobby",
- serverNameMatcher = ServerMatcherConfiguration(
- operation = MatcherType.STARTS_WITH,
- value = "silent"
- ),
- rules = listOf(
- RulesConfig(
- type = RulesConfig.Type.PERMISSION,
- name = "simplecloud.connection.silent",
- value = "true",
- )
- )
- )
- )
-
- val defaultTargetConnections = listOf(
- TargetConnectionConfig("lobby", 0),
- TargetConnectionConfig("hub", 0),
- TargetConnectionConfig("premium-lobby", 10),
- TargetConnectionConfig("vip-lobby", 20),
- TargetConnectionConfig("silent-lobby", 20)
- )
-
- val networkJoinTargets = TargetsConfig(
- enabled = true,
- noTargetConnectionFoundMessage = "Couldn't connect you to the network because\nno target servers are available.",
- targetConnections = defaultTargetConnections
- )
-
- val fallbackConnectionsConfig = TargetsConfig(
- enabled = true,
- noTargetConnectionFoundMessage = "You have been disconnected from the network\nbecause there are no fallback servers available.",
- targetConnections = defaultTargetConnections
- )
-
- val defaultCommands = listOf(
- CommandConfig(
- name = "lobby",
- alreadyConnectedMessage = "You are already connected to this lobby!",
- noTargetConnectionFound = "Couldn't find a target server!",
- targetConnections = defaultTargetConnections,
- aliases = listOf("l", "hub", "quit", "leave")
- )
- )
-
- return Config(
- connections = defaultConnections,
- networkJoinTargets = networkJoinTargets,
- fallbackConnectionsConfig = fallbackConnectionsConfig,
- commands = defaultCommands
- )
- }
- }
-}
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConfigFactory.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConfigFactory.kt
deleted file mode 100644
index 1fe06af..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConfigFactory.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package app.simplecloud.plugin.connection.shared.config
-
-import org.spongepowered.configurate.kotlin.extensions.get
-import org.spongepowered.configurate.kotlin.objectMapperFactory
-import org.spongepowered.configurate.kotlin.toNode
-import org.spongepowered.configurate.yaml.NodeStyle
-import org.spongepowered.configurate.yaml.YamlConfigurationLoader
-import java.nio.file.Files
-import java.nio.file.Path
-
-object ConfigFactory {
-
- fun loadOrCreate(dataDirectory: Path): Config {
- val path = dataDirectory.resolve("config.yml")
- val loader = YamlConfigurationLoader.builder()
- .path(path)
- .nodeStyle(NodeStyle.BLOCK)
- .defaultOptions { options ->
- options.serializers {
- it.registerAnnotatedObjects(objectMapperFactory()).build()
- }
- }
- .build()
-
- if (!Files.exists(path)) {
- return create(path, loader)
- }
-
- val configurationNode = loader.load()
- return configurationNode.get() ?: throw IllegalStateException("Config could not be loaded")
- }
-
-
- private fun create(path: Path, loader: YamlConfigurationLoader): Config {
- val config = Config.createDefaultConfig()
- if (!Files.exists(path)) {
- path.parent?.let { Files.createDirectories(it) }
- Files.createFile(path)
-
- val configurationNode = loader.load()
- config.toNode(configurationNode)
- loader.save(configurationNode)
- }
-
- return config
- }
-
-}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConnectionConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConnectionConfig.kt
index 5559623..6092424 100644
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConnectionConfig.kt
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/ConnectionConfig.kt
@@ -1,11 +1,79 @@
package app.simplecloud.plugin.connection.shared.config
+import app.simplecloud.plugin.api.shared.matcher.OperationType
import app.simplecloud.plugin.api.shared.matcher.ServerMatcherConfiguration
+import app.simplecloud.plugin.connection.shared.utilities.ConfigVersion
+import app.simplecloud.plugin.connection.shared.utilities.DefaultConfigs
import org.spongepowered.configurate.objectmapping.ConfigSerializable
@ConfigSerializable
data class ConnectionConfig(
+ val version: Char = ConfigVersion.VERSION,
+ val registration: RegistrationConfig = RegistrationConfig(),
+ val connections: List = DefaultConfigs.CONNECTIONS,
+ val networkJoinTargets: NetworkJoinTargetsConfig = DefaultConfigs.NETWORK_JOIN_TARGETS,
+ val fallback: FallbackConfig = DefaultConfigs.FALLBACK,
+)
+
+@ConfigSerializable
+data class RegistrationConfig(
+ val enabled: Boolean = true,
+ val serverNamePattern: String = "-",
+ val persistentServerNamePattern: String = "",
+ val ignoreServerGroupsAndPersistentServers: List = listOf(),
+ val additionalServers: List = listOf()
+)
+
+@ConfigSerializable
+data class RegistrationServer(
+ val name: String = "",
+ val address: String = "",
+ val port: Long = 0L
+)
+
+@ConfigSerializable
+data class ConnectionEntry(
val name: String = "",
val serverNameMatcher: ServerMatcherConfiguration = ServerMatcherConfiguration(),
- val rules: List = emptyList()
+ val rules: List = listOf(),
+)
+
+@ConfigSerializable
+data class ConnectionRule(
+ val type: RuleType = RuleType.PERMISSION,
+ val name: String = "",
+ val value: String = "",
+ val operation: OperationType = OperationType.EQUALS,
+ val negate: Boolean = false,
+ val bypassPermission: String = "",
+)
+
+enum class RuleType {
+ PERMISSION,
+ ENV
+}
+
+@ConfigSerializable
+data class NetworkJoinTargetsConfig(
+ val enabled: Boolean = true,
+ val targetConnections: List = listOf(),
+)
+
+@ConfigSerializable
+data class TargetConnection(
+ val name: String = "",
+ val priority: Int = 0,
+)
+
+@ConfigSerializable
+data class FallbackConfig(
+ val enabled: Boolean = true,
+ val targetConnections: List = listOf(),
+)
+
+@ConfigSerializable
+data class FallbackTargetConnection(
+ val name: String = "",
+ val priority: Int = 0,
+ val from: List = listOf(),
)
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/MessageConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/MessageConfig.kt
new file mode 100644
index 0000000..1c65fb7
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/MessageConfig.kt
@@ -0,0 +1,43 @@
+package app.simplecloud.plugin.connection.shared.config
+
+import app.simplecloud.plugin.connection.shared.utilities.ConfigVersion
+import app.simplecloud.plugin.connection.shared.utilities.DefaultConfigs
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.minimessage.MiniMessage
+import net.kyori.adventure.text.minimessage.tag.Tag
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver
+import org.spongepowered.configurate.objectmapping.ConfigSerializable
+
+@ConfigSerializable
+data class MessageConfig(
+ val version: Char = ConfigVersion.VERSION,
+ val variables: Map = DefaultConfigs.VARIABLES,
+ val kick: KickMessages = KickMessages(),
+ val command: ConnectionCommandMessages = ConnectionCommandMessages()
+) {
+ private val miniMessage = MiniMessage.miniMessage()
+
+ private fun tagResolver(): TagResolver {
+ val resolvers = variables.map { (key, value) ->
+ TagResolver.resolver(key, Tag.selfClosingInserting(miniMessage.deserialize(value)))
+ }
+ return TagResolver.resolver(*resolvers.toTypedArray())
+ }
+
+ fun send(message: String, vararg tagResolver: TagResolver): Component =
+ miniMessage.deserialize(message, TagResolver.resolver(tagResolver(), *tagResolver))
+}
+
+@ConfigSerializable
+data class KickMessages(
+ val noFallbackServers: String = "There is no fallback server available.",
+ val noTargetConnection: String = "You have been disconnected from the network
because there are no fallback servers available.",
+)
+
+@ConfigSerializable
+data class ConnectionCommandMessages(
+ val commandUsage: String = " Usage: /connection reload",
+ val configReloading: String = " Reloading Connection configurations...",
+ val configReloadedSuccess: String = " Successfully reloaded all Connection configurations.",
+ val configReloadedFailed: String = " Failed to reload Connection configurations."
+)
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/RulesConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/RulesConfig.kt
deleted file mode 100644
index acc10ff..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/RulesConfig.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package app.simplecloud.plugin.connection.shared.config
-
-import app.simplecloud.plugin.api.shared.matcher.MatcherType
-import app.simplecloud.plugin.connection.shared.PermissionChecker
-import org.spongepowered.configurate.objectmapping.ConfigSerializable
-
-@ConfigSerializable
-data class RulesConfig(
- val type: Type = Type.ENV,
- val operation: MatcherType = MatcherType.STARTS_WITH,
- val name: String = "",
- val value: String = "",
- val negate: Boolean = false,
- val bypassPermission: String = ""
-) {
-
- enum class Type {
- ENV,
- PERMISSION
- }
-
- fun isAllowed(player: P, permissionChecker: PermissionChecker
): Boolean {
- if (bypassPermission.isNotEmpty() && permissionChecker.checkPermission(player, bypassPermission)) {
- return true
- }
-
- when (type) {
- Type.ENV -> {
- val env = System.getenv(name)
- return operation.matches(env, value, negate)
- }
-
- Type.PERMISSION -> {
- return permissionChecker.checkPermission(player, name).toString().equals(value, true)
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/TargetConnectionConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/TargetConnectionConfig.kt
deleted file mode 100644
index 9f20ebc..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/TargetConnectionConfig.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package app.simplecloud.plugin.connection.shared.config
-
-import app.simplecloud.plugin.api.shared.matcher.ServerMatcherConfiguration
-import org.spongepowered.configurate.objectmapping.ConfigSerializable
-
-@ConfigSerializable
-data class TargetConnectionConfig (
- val name: String = "",
- val priority: Int = 0,
- val from: List = emptyList()
-)
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/TargetsConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/TargetsConfig.kt
deleted file mode 100644
index e6a904e..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/TargetsConfig.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package app.simplecloud.plugin.connection.shared.config
-
-import org.spongepowered.configurate.objectmapping.ConfigSerializable
-
-@ConfigSerializable
-data class TargetsConfig(
- val enabled: Boolean = false,
- val noTargetConnectionFoundMessage: String = "",
- val targetConnections: List = emptyList()
-)
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/YamlConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/YamlConfig.kt
new file mode 100644
index 0000000..25875aa
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/YamlConfig.kt
@@ -0,0 +1,176 @@
+package app.simplecloud.plugin.connection.shared.config
+
+import app.simplecloud.plugin.connection.shared.config.reactive.ReactiveConfig
+import app.simplecloud.plugin.connection.shared.config.reactive.ReactiveConfigInfo
+import kotlinx.coroutines.*
+import org.apache.logging.log4j.LogManager
+import org.spongepowered.configurate.CommentedConfigurationNode
+import org.spongepowered.configurate.kotlin.objectMapperFactory
+import org.spongepowered.configurate.loader.ParsingException
+import org.spongepowered.configurate.yaml.NodeStyle
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader
+import java.io.File
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardWatchEventKinds
+import java.util.concurrent.ConcurrentHashMap
+
+open class YamlConfig(private val dirPath: String) {
+
+ private val logger = LogManager.getLogger(YamlConfig::class.java)
+ private val watchService = FileSystems.getDefault().newWatchService()
+ private val configCache = ConcurrentHashMap()
+ private val reactiveConfigs = ConcurrentHashMap>>()
+ private var watcherJob: Job? = null
+
+ init {
+ startWatcher()
+ }
+
+ inline fun load(): ReactiveConfig {
+ return load(null)
+ }
+
+ inline fun load(path: String?): ReactiveConfig {
+ return ReactiveConfig(this, path, T::class.java)
+ }
+
+ internal fun loadDirect(path: String?, clazz: Class): T? {
+ val cacheKey = path ?: "default"
+
+ try {
+ val node = buildNode(path).first
+ val config = node.get(clazz)
+
+ if (config != null) {
+ configCache[cacheKey] = config
+ }
+
+ return config
+ } catch (ex: ParsingException) {
+ val file = File(if (path != null) "${dirPath}/${path.lowercase()}.yml" else dirPath)
+ logger.warn("Could not load config file ${file.name}. Using cached version if available.")
+ @Suppress("UNCHECKED_CAST")
+ return configCache[cacheKey] as? T
+ }
+ }
+
+ internal fun registerReactiveConfig(path: String?, clazz: Class, reactiveConfig: ReactiveConfig) {
+ val cacheKey = path ?: "default"
+ val configs = reactiveConfigs.getOrPut(cacheKey) { mutableListOf() }
+ configs.add(ReactiveConfigInfo(clazz, reactiveConfig))
+ }
+
+ private fun buildNode(path: String?): Pair {
+ val file = File(if (path != null) "${dirPath}/${path.lowercase()}.yml" else dirPath)
+ if (!file.exists()) {
+ file.parentFile.mkdirs()
+ file.createNewFile()
+ }
+ val loader = YamlConfigurationLoader.builder()
+ .path(file.toPath())
+ .nodeStyle(NodeStyle.BLOCK)
+ .defaultOptions { options ->
+ options.serializers { builder ->
+ builder.registerAnnotatedObjects(objectMapperFactory())
+ }
+ }.build()
+ return Pair(loader.load(), loader)
+ }
+
+ fun save(obj: T) {
+ save(null, obj)
+ }
+
+ private fun save(path: String?, obj: T) {
+ val pair = buildNode(path)
+ pair.first.set(obj)
+ pair.second.save(pair.first)
+
+ // Update cache after successful save
+ val cacheKey = path ?: "default"
+ if (obj != null) {
+ configCache[cacheKey] = obj
+ }
+ }
+
+ fun save(path: String?, reactiveConfig: ReactiveConfig) {
+ return save(path, reactiveConfig.get())
+ }
+
+ private fun startWatcher() {
+ val directory = File(dirPath).toPath()
+ if (!directory.toFile().exists()) {
+ directory.toFile().mkdirs()
+ }
+
+ try {
+ directory.register(
+ watchService,
+ StandardWatchEventKinds.ENTRY_MODIFY
+ )
+
+ watcherJob = CoroutineScope(Dispatchers.IO).launch {
+ while (isActive) {
+ try {
+ val key = watchService.take()
+ for (event in key.pollEvents()) {
+ val path = event.context() as? Path ?: continue
+ val resolvedPath = directory.resolve(path)
+
+ if (Files.isDirectory(resolvedPath) || !resolvedPath.toString().endsWith(".yml")) {
+ continue
+ }
+
+ val kind = event.kind()
+ if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
+ handleFileChange(resolvedPath.toFile())
+ }
+ }
+ key.reset()
+ } catch (e: Exception) {
+ logger.warn("Error in config watcher: ${e.message}")
+ }
+ }
+ }
+ } catch (e: Exception) {
+ logger.warn("Could not start config file watcher: ${e.message}")
+ }
+ }
+
+ private fun handleFileChange(file: File) {
+ val fileName = file.nameWithoutExtension
+ val cacheKey = if (file.name == File(dirPath).name) "default" else fileName
+
+ val reactiveConfigList = reactiveConfigs[cacheKey] ?: return
+
+ reactiveConfigList.forEach { configInfo ->
+ try {
+ val configPath = if (cacheKey == "default") null else fileName
+
+ @Suppress("UNCHECKED_CAST")
+ val typedConfigInfo = configInfo as ReactiveConfigInfo
+ val newValue = loadDirect(configPath, typedConfigInfo.clazz)
+
+ typedConfigInfo.reactiveConfig.update(newValue)
+
+ } catch (ex: ParsingException) {
+ logger.warn("Config file ${file.name} has parsing errors. Keeping old version.")
+ // Don't update the reactive config, keep the old cached version
+ } catch (e: Exception) {
+ logger.warn("Error updating reactive config: ${e.message}")
+ }
+ }
+ }
+
+ fun close() {
+ watcherJob?.cancel()
+ try {
+ watchService.close()
+ } catch (e: Exception) {
+ logger.warn("Error closing watch service: ${e.message}")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/reactive/ReactiveConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/reactive/ReactiveConfig.kt
new file mode 100644
index 0000000..90076b8
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/reactive/ReactiveConfig.kt
@@ -0,0 +1,29 @@
+package app.simplecloud.plugin.connection.shared.config.reactive
+
+import app.simplecloud.plugin.connection.shared.config.YamlConfig
+
+class ReactiveConfig(
+ val config: YamlConfig,
+ val path: String?,
+ val clazz: Class
+) {
+
+ @Volatile
+ private var currentValue: T? = config.loadDirect(path, clazz)
+
+ init {
+ config.registerReactiveConfig(path, clazz, this)
+ }
+
+ fun get(): T = currentValue?: throw NullPointerException("Reactive config is not initialized")
+
+ internal fun update(newValue: T?) {
+ currentValue = newValue
+ }
+
+ fun reload() {
+ val newValue = config.loadDirect(path, clazz)
+ update(newValue)
+ }
+
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/reactive/ReactiveConfigInfo.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/reactive/ReactiveConfigInfo.kt
new file mode 100644
index 0000000..1509cfa
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/reactive/ReactiveConfigInfo.kt
@@ -0,0 +1,6 @@
+package app.simplecloud.plugin.connection.shared.config.reactive
+
+data class ReactiveConfigInfo(
+ val clazz: Class,
+ val reactiveConfig: ReactiveConfig
+)
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/connection/ConnectionResolver.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/connection/ConnectionResolver.kt
new file mode 100644
index 0000000..4d5a643
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/connection/ConnectionResolver.kt
@@ -0,0 +1,58 @@
+package app.simplecloud.plugin.connection.shared.connection
+
+import app.simplecloud.plugin.connection.shared.config.ConnectionEntry
+import app.simplecloud.plugin.connection.shared.config.ConnectionRule
+import app.simplecloud.plugin.connection.shared.config.RuleType
+
+object ConnectionResolver {
+
+ fun findConnection(name: String, connections: List): ConnectionEntry? {
+ return connections.find { it.name.equals(name, ignoreCase = true) }
+ }
+
+ fun findMatchingServerNames(
+ connection: ConnectionEntry,
+ servers: List,
+ ): List {
+ return servers.filter { connection.serverNameMatcher.matches(it) }
+ }
+
+ fun checkRules(
+ connection: ConnectionEntry,
+ permissionChecker: (String) -> Boolean,
+ ): ConnectionRule? {
+ for (rule in connection.rules) {
+ if (rule.bypassPermission.isNotEmpty() && permissionChecker(rule.bypassPermission)) {
+ continue
+ }
+
+ val failed = when (rule.type) {
+ RuleType.PERMISSION -> {
+ val hasPermission = permissionChecker(rule.name)
+ hasPermission != rule.value.toBoolean()
+ }
+
+ RuleType.ENV -> {
+ val envValue = System.getenv(rule.name) ?: ""
+ val matches = rule.operation.matches(envValue, rule.value, rule.negate)
+ !matches
+ }
+ }
+
+ if (failed) return rule
+ }
+ return null
+ }
+
+ fun isServerInConnection(
+ serverName: String,
+ connectionName: String,
+ connections: List,
+ servers: List,
+ ): Boolean {
+ val connection = findConnection(connectionName, connections) ?: return false
+ val matchingNames = findMatchingServerNames(connection, servers)
+ return matchingNames.any { it.equals(serverName, ignoreCase = true) }
+ }
+
+}
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/listener/ServerEventListener.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/listener/ServerEventListener.kt
new file mode 100644
index 0000000..5fe7c28
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/listener/ServerEventListener.kt
@@ -0,0 +1,91 @@
+package app.simplecloud.plugin.connection.shared.listener
+
+import app.simplecloud.api.CloudApi
+import app.simplecloud.api.event.Subscription
+import app.simplecloud.api.group.GroupServerType
+import app.simplecloud.api.server.Server
+import app.simplecloud.api.server.ServerState
+import app.simplecloud.plugin.connection.shared.registration.RegisteredServer
+import app.simplecloud.plugin.connection.shared.registration.ServerRegistry
+import kotlinx.coroutines.*
+import org.apache.logging.log4j.LogManager
+
+class ServerEventListener(
+ private val api: CloudApi,
+ private val registry: ServerRegistry,
+) {
+
+ private val logger = LogManager.getLogger(ServerEventListener::class.java)
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private var subscriptions: MutableList = mutableListOf()
+
+ fun start() {
+ subscriptions.add(
+ api.event().server().onStateChanged { event ->
+ val server = event.server ?: return@onStateChanged
+ if (server.serverBase?.type != GroupServerType.SERVER) return@onStateChanged
+ if (event.newState == ServerState.AVAILABLE && event.oldState != ServerState.AVAILABLE) {
+ scope.launch {
+ register(convertToRegisteredServer(server))
+ }
+ }
+ }
+ )
+
+ subscriptions.add(
+ api.event().server().onStopped { event ->
+ val server = event.server ?: return@onStopped
+ scope.launch {
+ unregister(convertToRegisteredServer(server))
+ }
+ }
+ )
+ }
+
+ fun stop() {
+ subscriptions.forEach { it.close() }
+ subscriptions.clear()
+ scope.cancel()
+ }
+
+ fun register(server: Server) {
+ register(convertToRegisteredServer(server))
+ }
+
+ private fun register(server: RegisteredServer) {
+ if (server.blueprintConfigurator == "standalone") return
+
+ if (server.persistent) {
+ logger.info("Registering server ${server.serverId} (${server.serverBaseName})...")
+ } else {
+ logger.info("Registering server ${server.serverId} (${server.serverBaseName}-${server.numericalId})...")
+ }
+
+ registry.register(server)
+ }
+
+ private fun unregister(server: RegisteredServer) {
+ if (registry.getRegistered().containsKey(server.serverId)) {
+ if (server.persistent) {
+ logger.info("Unregistering server ${server.serverId} (${server.serverBaseName})...")
+ } else {
+ logger.info("Unregistering server ${server.serverId} (${server.serverBaseName}-${server.numericalId})...")
+ }
+
+ registry.unregister(server)
+ }
+ }
+
+ private fun convertToRegisteredServer(server: Server): RegisteredServer {
+ return RegisteredServer(
+ serverId = server.serverId,
+ numericalId = server.numericalId,
+ ip = server.ip!!,
+ port = server.port!!,
+ serverBaseName = server.serverBase!!.name!!,
+ properties = server.properties ?: emptyMap(),
+ blueprintConfigurator = server.blueprint?.configurator,
+ persistent = server.isFromPersistentServer
+ )
+ }
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/registration/RegisteredServer.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/registration/RegisteredServer.kt
new file mode 100644
index 0000000..e0a9231
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/registration/RegisteredServer.kt
@@ -0,0 +1,15 @@
+package app.simplecloud.plugin.connection.shared.registration
+
+/**
+ * Represents a registered server.
+ */
+data class RegisteredServer(
+ val serverId: String,
+ val numericalId: Int,
+ val ip: String,
+ val port: Int,
+ val serverBaseName: String,
+ val properties: Map,
+ val blueprintConfigurator: String?,
+ val persistent: Boolean,
+)
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/registration/ServerRegistry.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/registration/ServerRegistry.kt
new file mode 100644
index 0000000..da53368
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/registration/ServerRegistry.kt
@@ -0,0 +1,28 @@
+package app.simplecloud.plugin.connection.shared.registration
+
+/**
+ * Manages server registration and lifecycle within the proxy network.
+ */
+interface ServerRegistry {
+
+ /**
+ * Retrieves all currently registered servers.
+ *
+ * @return map of server IDs to their registered server instances
+ */
+ fun getRegistered(): Map
+
+ /**
+ * Registers a new server.
+ *
+ * @param server The server to register
+ */
+ fun register(server: RegisteredServer)
+
+ /**
+ * Unregisters a server.
+ *
+ * @param server The server to unregister
+ */
+ fun unregister(server: RegisteredServer)
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/resolver/RegisteredServerResolver.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/resolver/RegisteredServerResolver.kt
new file mode 100644
index 0000000..981c84c
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/resolver/RegisteredServerResolver.kt
@@ -0,0 +1,30 @@
+package app.simplecloud.plugin.connection.shared.resolver
+
+import app.simplecloud.plugin.connection.shared.config.RegistrationConfig
+import app.simplecloud.plugin.connection.shared.registration.RegisteredServer
+import net.kyori.adventure.text.minimessage.MiniMessage
+import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
+import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
+
+object RegisteredServerResolver {
+
+ private val serializer = PlainTextComponentSerializer.plainText()
+
+ fun resolve(server: RegisteredServer, config: RegistrationConfig): String {
+ val pattern = if (!server.persistent) config.serverNamePattern else config.persistentServerNamePattern
+
+ val resolver = TagResolver.resolver(
+ Placeholder.unparsed("group", server.serverBaseName),
+ Placeholder.unparsed("name", server.serverBaseName),
+ Placeholder.unparsed("numerical_id", server.numericalId.toString()),
+ Placeholder.unparsed("id", server.serverId),
+ *server.properties.map { (key, value) ->
+ Placeholder.unparsed(key.lowercase(), value.toString())
+ }.toTypedArray()
+ )
+
+ val component = MiniMessage.miniMessage().deserialize(pattern, resolver)
+ return serializer.serialize(component)
+ }
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/server/ServerConnectionInfo.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/server/ServerConnectionInfo.kt
deleted file mode 100644
index af2f15b..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/server/ServerConnectionInfo.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package app.simplecloud.plugin.connection.shared.server
-
-data class ServerConnectionInfo(
- val name: String,
- val onlinePlayers: Int
-)
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/server/ServerConnectionInfoGetter.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/server/ServerConnectionInfoGetter.kt
deleted file mode 100644
index ef97f6d..0000000
--- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/server/ServerConnectionInfoGetter.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package app.simplecloud.plugin.connection.shared.server
-
-fun interface ServerConnectionInfoGetter {
-
- fun get(): List
-
-}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/utilities/ConfigVersion.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/utilities/ConfigVersion.kt
new file mode 100644
index 0000000..2b33539
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/utilities/ConfigVersion.kt
@@ -0,0 +1,7 @@
+package app.simplecloud.plugin.connection.shared.utilities
+
+object ConfigVersion {
+
+ const val VERSION = '1'
+
+}
\ No newline at end of file
diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/utilities/DefaultConfigs.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/utilities/DefaultConfigs.kt
new file mode 100644
index 0000000..c60c68a
--- /dev/null
+++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/utilities/DefaultConfigs.kt
@@ -0,0 +1,63 @@
+package app.simplecloud.plugin.connection.shared.utilities
+
+import app.simplecloud.plugin.api.shared.matcher.OperationType
+import app.simplecloud.plugin.api.shared.matcher.ServerMatcherConfiguration
+import app.simplecloud.plugin.connection.shared.config.*
+
+object DefaultConfigs {
+
+ val VARIABLES: Map = mapOf("prefix" to "⚡")
+
+ val CONNECTIONS: List = listOf(
+ ConnectionEntry(
+ name = "lobby",
+ serverNameMatcher = ServerMatcherConfiguration(
+ operation = OperationType.STARTS_WITH,
+ value = "lobby",
+ negate = false,
+ ),
+ rules = listOf(),
+ ),
+ )
+
+ val NETWORK_JOIN_TARGETS: NetworkJoinTargetsConfig = NetworkJoinTargetsConfig(
+ enabled = true,
+ targetConnections = listOf(
+ TargetConnection(
+ name = "lobby",
+ priority = 0,
+ ),
+ ),
+ )
+
+ val FALLBACK: FallbackConfig = FallbackConfig(
+ enabled = true,
+ targetConnections = listOf(
+ FallbackTargetConnection(
+ name = "lobby",
+ priority = 0,
+ from = listOf(),
+ ),
+ ),
+ )
+
+ val COMMANDS: List = listOf(
+ CommandEntry(
+ name = "lobby",
+ aliases = listOf("l", "hub", "quit", "leave"),
+ targetConnections = listOf(
+ FallbackTargetConnection(
+ name = "lobby",
+ priority = 0,
+ from = listOf(),
+ ),
+ ),
+ messages = CommandMessages(
+ alreadyConnected = "You are already connected to this lobby!",
+ noTargetConnectionFound = "Couldn't find a target server!",
+ ),
+ permission = "",
+ ),
+ )
+
+}
\ No newline at end of file
diff --git a/connection-velocity/build.gradle.kts b/connection-velocity/build.gradle.kts
index 3afe125..fede50a 100644
--- a/connection-velocity/build.gradle.kts
+++ b/connection-velocity/build.gradle.kts
@@ -4,9 +4,10 @@ plugins {
}
dependencies {
- api(project(":connection-shared"))
- compileOnly("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
- kapt("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT")
+ implementation(project(":connection-shared"))
+ compileOnly(libs.simplecloud.api)
+ compileOnly(libs.velocity.api)
+ kapt(libs.velocity.api)
}
modrinth {
@@ -16,9 +17,6 @@ modrinth {
versionType.set("beta")
uploadFile.set(tasks.shadowJar)
gameVersions.addAll(
-
-
-
"1.20",
"1.20.1",
"1.20.2",
@@ -38,10 +36,7 @@ modrinth {
"1.21.9",
"1.21.10",
"1.21.11",
-
-
-
- )
+ )
loaders.add("velocity")
changelog.set("https://docs.simplecloud.app/changelog")
syncBodyFrom.set(rootProject.file("README.md").readText())
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/VelocityConnectionPlugin.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/VelocityConnectionPlugin.kt
new file mode 100644
index 0000000..25b358e
--- /dev/null
+++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/VelocityConnectionPlugin.kt
@@ -0,0 +1,98 @@
+package app.simplecloud.plugin.connection.velocity
+
+import app.simplecloud.api.CloudApi
+import app.simplecloud.plugin.connection.shared.ConnectionPlugin
+import app.simplecloud.plugin.connection.velocity.command.ConnectionCommand
+import app.simplecloud.plugin.connection.velocity.command.VelocityCommandManager
+import app.simplecloud.plugin.connection.velocity.listener.KickedFromServerListener
+import app.simplecloud.plugin.connection.velocity.listener.PlayerChooseInitialServerListener
+import app.simplecloud.plugin.connection.velocity.registration.VelocityServerRegistry
+import com.google.inject.Inject
+import com.velocitypowered.api.event.Subscribe
+import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
+import com.velocitypowered.api.event.proxy.ProxyShutdownEvent
+import com.velocitypowered.api.plugin.Plugin
+import com.velocitypowered.api.plugin.annotation.DataDirectory
+import com.velocitypowered.api.proxy.ProxyServer
+import com.velocitypowered.api.proxy.server.ServerInfo
+import kotlinx.coroutines.*
+import org.apache.logging.log4j.LogManager
+import java.net.InetSocketAddress
+import java.nio.file.Path
+
+@Plugin(
+ id = "simplecloud-connection",
+ name = "simplecloud-connection",
+ version = "1.0.0",
+ authors = ["Fllip", "hmtill"],
+ url = "https://github.com/simplecloudapp/server-connection-plugin"
+)
+class VelocityConnectionPlugin @Inject constructor(
+ @DataDirectory val dataDirectory: Path,
+ private val server: ProxyServer,
+) {
+
+ private val api = CloudApi.create()
+ private val logger = LogManager.getLogger(VelocityConnectionPlugin::class.java)
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private val registry = VelocityServerRegistry(this, server)
+ private val commandManager = VelocityCommandManager(this, server)
+
+ val connectionPlugin = ConnectionPlugin(
+ dataDirectory.toString(),
+ api,
+ registry
+ )
+
+ @Subscribe
+ fun onProxyInitialize(event: ProxyInitializeEvent) {
+ cleanupServers()
+ registerAdditionalServers()
+ registerListeners()
+ registerCommands()
+
+ scope.launch {
+ connectionPlugin.start()
+ }
+ }
+
+ @Subscribe
+ fun onProxyShutdown(event: ProxyShutdownEvent) {
+ commandManager.unregisterCommands()
+ scope.launch { connectionPlugin.shutdown() }
+ scope.cancel()
+ }
+
+ private fun cleanupServers() {
+ if (connectionPlugin.connectionConfig.get().registration.enabled) {
+ server.allServers.forEach {
+ server.unregisterServer(it.serverInfo)
+ }
+ }
+ }
+
+ private fun registerAdditionalServers() {
+ if (connectionPlugin.connectionConfig.get().registration.enabled) {
+ val additionalServers = connectionPlugin.connectionConfig.get().registration.additionalServers
+ additionalServers.forEach {
+ val info = ServerInfo(it.name, InetSocketAddress.createUnresolved(it.address, it.port.toInt()))
+ server.registerServer(info)
+ logger.info("Additional server ${info.name} has been registered!")
+ }
+ }
+ }
+
+ private fun registerListeners() {
+ server.eventManager.register(this, PlayerChooseInitialServerListener(this, server))
+ server.eventManager.register(this, KickedFromServerListener(this, server))
+ }
+
+ private fun registerCommands() {
+ commandManager.registerCommands()
+
+ val meta = server.commandManager.metaBuilder("connection")
+ .plugin(this)
+ .build()
+ server.commandManager.register(meta, ConnectionCommand(this))
+ }
+}
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/VelocityServerConnectionPlugin.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/VelocityServerConnectionPlugin.kt
deleted file mode 100644
index ad6267b..0000000
--- a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/VelocityServerConnectionPlugin.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package app.simplecloud.plugin.connection.velocity
-
-import app.simplecloud.plugin.connection.shared.PermissionChecker
-import app.simplecloud.plugin.connection.shared.ServerConnectionPlugin
-import app.simplecloud.plugin.connection.shared.config.CommandConfig
-import app.simplecloud.plugin.connection.shared.server.ServerConnectionInfo
-import app.simplecloud.plugin.connection.shared.server.ServerConnectionInfoGetter
-import com.google.inject.Inject
-import com.velocitypowered.api.command.BrigadierCommand
-import com.velocitypowered.api.event.Subscribe
-import com.velocitypowered.api.event.player.KickedFromServerEvent
-import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent
-import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
-import com.velocitypowered.api.plugin.Dependency
-import com.velocitypowered.api.plugin.Plugin
-import com.velocitypowered.api.plugin.annotation.DataDirectory
-import com.velocitypowered.api.proxy.Player
-import com.velocitypowered.api.proxy.ProxyServer
-import net.kyori.adventure.text.minimessage.MiniMessage
-import java.nio.file.Path
-import java.util.logging.Logger
-import kotlin.jvm.optionals.getOrNull
-
-@Plugin(
- id = "simplecloud-connection",
- name = "simplecloud-connection",
- version = "0.0.1",
- authors = ["Fllip", "hmtill"],
- dependencies = [
- Dependency(
- id = "simplecloud-api"
- )
- ],
- url = "https://github.com/theSimpleCloud/server-connection-plugin"
-)
-class VelocityServerConnectionPlugin @Inject constructor(
- @DataDirectory val dataDirectory: Path,
- private val server: ProxyServer,
- private val logger: Logger
-) {
-
- private val serverConnection = ServerConnectionPlugin(
- dataDirectory,
- ServerConnectionInfoGetter {
- server.allServers.map {
- ServerConnectionInfo(
- it.serverInfo.name,
- it.playersConnected.size
- )
- }
- },
- PermissionChecker { player, permission -> player.hasPermission(permission) }
- )
-
- private val miniMessage = MiniMessage.miniMessage()
-
- @Subscribe
- fun onProxyInitialize(event: ProxyInitializeEvent) {
- registerCommands()
- }
-
- @Subscribe
- fun onPlayerChooseInitialServer(event: PlayerChooseInitialServerEvent) {
- val serverConnectionInfoName = serverConnection.getServerNameForLogin(event.player)
- if (serverConnectionInfoName == null) {
- event.player.disconnect(miniMessage.deserialize(
- serverConnection.config.networkJoinTargets.noTargetConnectionFoundMessage
- ))
- return
- }
-
- val serverInfo = server.getServer(serverConnectionInfoName)
- serverInfo.ifPresent {
- event.setInitialServer(it)
- }
- }
-
- @Subscribe
- fun onKickedFromServer(event: KickedFromServerEvent) {
- val connectionAndTargetConfigToServerName = serverConnection.getConnectionAndNameForFallback(event.player, event.server.serverInfo.name)
- if (connectionAndTargetConfigToServerName == null) {
- event.result = KickedFromServerEvent.DisconnectPlayer.create(miniMessage.deserialize(
- serverConnection.config.fallbackConnectionsConfig.noTargetConnectionFoundMessage
- ))
- return
- }
-
- val (_, serverName) = connectionAndTargetConfigToServerName
- if (event.server.serverInfo.name == serverName) {
- return
- }
-
- if (event.player.currentServer.isEmpty) {
- return
- }
-
- server.getServer(serverName).ifPresent {
- event.result = KickedFromServerEvent.RedirectPlayer.create(it)
- }
- }
-
- private fun registerCommands() {
- val commandManager = server.commandManager
- serverConnection.getCommandConfigs().forEach {
- val commandMeta = commandManager.metaBuilder(it.name)
- .aliases(*it.aliases.toTypedArray())
- .plugin(this)
- .build()
-
- val commandToRegister = createCommand(it)
- commandManager.register(commandMeta, commandToRegister)
- }
- }
-
- private fun createCommand(commandConfig: CommandConfig): BrigadierCommand {
- val commandNode = BrigadierCommand.literalArgumentBuilder(commandConfig.name)
- .requires { commandConfig.permission.isEmpty() || it.hasPermission(commandConfig.permission) }
- .executes {
- val player = it.source as? Player ?: return@executes 0
- val currentServerName = player.currentServer.getOrNull()?.serverInfo?.name
- val connectionToServerName = serverConnection.getConnectionAndNameForCommand(
- player,
- commandConfig,
- )
-
- if (connectionToServerName == null) {
- player.sendMessage(miniMessage.deserialize(commandConfig.noTargetConnectionFound))
- return@executes 1
- }
-
- if (currentServerName != null
- && connectionToServerName.first.connectionConfig.serverNameMatcher.matches(currentServerName)
- ) {
- player.sendMessage(miniMessage.deserialize(commandConfig.alreadyConnectedMessage))
- return@executes 1
- }
-
- val registeredServer = server.getServer(connectionToServerName.second)
- registeredServer.ifPresent {
- player.createConnectionRequest(it).fireAndForget()
- }
-
- return@executes 1
- }
- .build()
-
- return BrigadierCommand(commandNode)
- }
-
-}
\ No newline at end of file
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/ConnectionCommand.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/ConnectionCommand.kt
new file mode 100644
index 0000000..3944f8a
--- /dev/null
+++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/ConnectionCommand.kt
@@ -0,0 +1,46 @@
+package app.simplecloud.plugin.connection.velocity.command
+
+import app.simplecloud.plugin.connection.velocity.VelocityConnectionPlugin
+import com.velocitypowered.api.command.SimpleCommand
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.apache.logging.log4j.LogManager
+
+class ConnectionCommand(
+ private val plugin: VelocityConnectionPlugin,
+) : SimpleCommand {
+
+ private val logger = LogManager.getLogger(ConnectionCommand::class.java)
+
+ override fun execute(invocation: SimpleCommand.Invocation) {
+ val args = invocation.arguments()
+ val source = invocation.source()
+
+ val messages = plugin.connectionPlugin.messageConfig.get()
+
+ if (args.isEmpty() || !args[0].equals("reload", ignoreCase = true)) {
+ source.sendMessage(messages.send(messages.command.commandUsage))
+ return
+ }
+
+ source.sendMessage(messages.send(messages.command.configReloading))
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ plugin.connectionPlugin.connectionConfig.reload()
+ plugin.connectionPlugin.commandConfig.reload()
+ plugin.connectionPlugin.messageConfig.reload()
+
+ source.sendMessage(messages.send(messages.command.configReloadedSuccess))
+ }
+ } catch (e: Exception) {
+ source.sendMessage(messages.send(messages.command.configReloadedFailed))
+ logger.error("Failed to reload config", e)
+ }
+ }
+
+ override fun hasPermission(invocation: SimpleCommand.Invocation): Boolean {
+ return invocation.source().hasPermission("simplecloud.connection.reload")
+ }
+
+}
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/VelocityCommandManager.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/VelocityCommandManager.kt
new file mode 100644
index 0000000..7ea1754
--- /dev/null
+++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/VelocityCommandManager.kt
@@ -0,0 +1,106 @@
+package app.simplecloud.plugin.connection.velocity.command
+
+import app.simplecloud.plugin.connection.shared.config.CommandEntry
+import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver
+import app.simplecloud.plugin.connection.velocity.VelocityConnectionPlugin
+import com.velocitypowered.api.command.SimpleCommand
+import com.velocitypowered.api.proxy.Player
+import com.velocitypowered.api.proxy.ProxyServer
+
+class VelocityCommandManager(
+ private val plugin: VelocityConnectionPlugin,
+ private val proxy: ProxyServer,
+) {
+ private val commands = mutableListOf()
+
+ fun registerCommands() {
+ val commands = plugin.connectionPlugin.commandConfig.get().commands
+ for (command in commands) {
+ registerCommand(command)
+ }
+ }
+
+ fun unregisterCommands() {
+ commands.forEach { proxy.commandManager.unregister(it) }
+ commands.clear()
+ }
+
+ private fun registerCommand(command: CommandEntry) {
+ val connectionCommand = ConnectionCommand(plugin, proxy, command)
+ val meta = proxy.commandManager.metaBuilder(command.name)
+ .aliases(*command.aliases.toTypedArray())
+ .plugin(plugin)
+ .build()
+
+ proxy.commandManager.register(meta, connectionCommand)
+ commands.add(command.name)
+ }
+
+ private class ConnectionCommand(
+ private val plugin: VelocityConnectionPlugin,
+ private val proxy: ProxyServer,
+ private val command: CommandEntry,
+ ) : SimpleCommand {
+
+ override fun execute(invocation: SimpleCommand.Invocation) {
+ val source = invocation.source()
+ if (source !is Player) return
+
+ val config = plugin.connectionPlugin.connectionConfig.get()
+ val messages = plugin.connectionPlugin.messageConfig.get()
+
+ if (command.permission.isNotEmpty() && !source.hasPermission(command.permission)) {
+ return
+ }
+
+ val currentServerName = source.currentServer.orElse(null)?.serverInfo?.name
+ val serverNames = proxy.allServers.map { it.serverInfo.name }
+ val sortedTargets = command.targetConnections.sortedByDescending { it.priority }
+
+ for (target in sortedTargets) {
+ if (target.from.isNotEmpty() && currentServerName != null) {
+ val isFromAllowed = target.from.any { connectionName ->
+ ConnectionResolver.isServerInConnection(
+ currentServerName, connectionName, config.connections, serverNames
+ )
+ }
+ if (!isFromAllowed) continue
+ }
+
+ val connection = ConnectionResolver.findConnection(target.name, config.connections) ?: continue
+
+ val failedRule = ConnectionResolver.checkRules(connection) { permission ->
+ source.hasPermission(permission)
+ }
+ if (failedRule != null) {
+ return
+ }
+
+ val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames)
+ if (matchingNames.isEmpty()) continue
+
+ val server = matchingNames
+ .mapNotNull { proxy.getServer(it).orElse(null) }
+ .minByOrNull { it.playersConnected.size }
+ ?: continue
+
+ if (currentServerName != null && server.serverInfo.name == currentServerName) {
+ source.sendMessage(messages.send(command.messages.alreadyConnected))
+ return
+ }
+
+ source.createConnectionRequest(server).fireAndForget()
+ return
+ }
+
+ source.sendMessage(messages.send(command.messages.noTargetConnectionFound))
+ }
+
+ override fun hasPermission(invocation: SimpleCommand.Invocation): Boolean {
+ if (command.permission.isEmpty()) return true
+ return invocation.source().hasPermission(command.permission)
+ }
+
+ }
+
+}
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/KickedFromServerListener.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/KickedFromServerListener.kt
new file mode 100644
index 0000000..fdb1db4
--- /dev/null
+++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/KickedFromServerListener.kt
@@ -0,0 +1,60 @@
+package app.simplecloud.plugin.connection.velocity.listener
+
+import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver
+import app.simplecloud.plugin.connection.velocity.VelocityConnectionPlugin
+import com.velocitypowered.api.event.Subscribe
+import com.velocitypowered.api.event.player.KickedFromServerEvent
+import com.velocitypowered.api.proxy.ProxyServer
+
+class KickedFromServerListener(
+ private val plugin: VelocityConnectionPlugin,
+ private val proxy: ProxyServer,
+) {
+
+ @Subscribe
+ fun onKickedFromServer(event: KickedFromServerEvent) {
+ val config = plugin.connectionPlugin.connectionConfig.get()
+ val messages = plugin.connectionPlugin.messageConfig.get()
+
+ if (!config.fallback.enabled) return
+
+ val kickedServerName = event.server.serverInfo.name
+ val serverNames = proxy.allServers.map { it.serverInfo.name }
+ val sortedTargets = config.fallback.targetConnections.sortedByDescending { it.priority }
+
+ for (target in sortedTargets) {
+ if (target.from.isNotEmpty()) {
+ val isFromAllowed = target.from.any { connectionName ->
+ ConnectionResolver.isServerInConnection(
+ kickedServerName, connectionName, config.connections, serverNames
+ )
+ }
+ if (!isFromAllowed) continue
+ }
+
+ val connection = ConnectionResolver.findConnection(target.name, config.connections) ?: continue
+
+ val failedRule = ConnectionResolver.checkRules(connection) { permission ->
+ event.player.hasPermission(permission)
+ }
+ if (failedRule != null) continue
+
+ val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames)
+ if (matchingNames.isEmpty()) continue
+
+ val server = matchingNames
+ .mapNotNull { proxy.getServer(it).orElse(null) }
+ .filter { it.serverInfo.name != kickedServerName }
+ .minByOrNull { it.playersConnected.size }
+ ?: continue
+
+ event.result = KickedFromServerEvent.RedirectPlayer.create(server)
+ return
+ }
+
+ event.result = KickedFromServerEvent.DisconnectPlayer.create(
+ messages.send(messages.kick.noFallbackServers)
+ )
+ }
+
+}
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/PlayerChooseInitialServerListener.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/PlayerChooseInitialServerListener.kt
new file mode 100644
index 0000000..8f5b188
--- /dev/null
+++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/PlayerChooseInitialServerListener.kt
@@ -0,0 +1,48 @@
+package app.simplecloud.plugin.connection.velocity.listener
+
+import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver
+import app.simplecloud.plugin.connection.velocity.VelocityConnectionPlugin
+import com.velocitypowered.api.event.Subscribe
+import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent
+import com.velocitypowered.api.proxy.ProxyServer
+
+class PlayerChooseInitialServerListener(
+ private val plugin: VelocityConnectionPlugin,
+ private val proxy: ProxyServer,
+) {
+
+ @Subscribe
+ fun onPlayerChooseInitialServer(event: PlayerChooseInitialServerEvent) {
+ val config = plugin.connectionPlugin.connectionConfig.get()
+ val messages = plugin.connectionPlugin.messageConfig.get()
+
+ if (!config.networkJoinTargets.enabled) return
+
+ val serverNames = proxy.allServers.map { it.serverInfo.name }
+ val sortedTargets = config.networkJoinTargets.targetConnections.sortedByDescending { it.priority }
+
+ for (target in sortedTargets) {
+ val connection = ConnectionResolver.findConnection(target.name, config.connections) ?: continue
+
+ val failedRule = ConnectionResolver.checkRules(connection) { permission ->
+ event.player.hasPermission(permission)
+ }
+ if (failedRule != null) continue
+
+ val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames)
+ if (matchingNames.isEmpty()) continue
+
+ val server = matchingNames
+ .mapNotNull { proxy.getServer(it).orElse(null) }
+ .minByOrNull { it.playersConnected.size }
+ ?: continue
+
+ event.setInitialServer(server)
+ return
+ }
+
+ event.setInitialServer(null)
+ event.player.disconnect(messages.send(messages.kick.noTargetConnection))
+ }
+
+}
diff --git a/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/registration/VelocityServerRegistry.kt b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/registration/VelocityServerRegistry.kt
new file mode 100644
index 0000000..5c2170b
--- /dev/null
+++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/registration/VelocityServerRegistry.kt
@@ -0,0 +1,39 @@
+package app.simplecloud.plugin.connection.velocity.registration
+
+import app.simplecloud.plugin.connection.shared.registration.RegisteredServer
+import app.simplecloud.plugin.connection.shared.registration.ServerRegistry
+import app.simplecloud.plugin.connection.shared.resolver.RegisteredServerResolver
+import app.simplecloud.plugin.connection.velocity.VelocityConnectionPlugin
+import com.velocitypowered.api.proxy.ProxyServer
+import com.velocitypowered.api.proxy.server.ServerInfo
+import java.net.InetSocketAddress
+import kotlin.jvm.optionals.getOrNull
+
+class VelocityServerRegistry(
+ private val plugin: VelocityConnectionPlugin,
+ private val proxy: ProxyServer
+) : ServerRegistry {
+
+ private val servers = mutableMapOf()
+
+ override fun getRegistered(): Map {
+ return servers
+ }
+
+ override fun register(server: RegisteredServer) {
+ val info = ServerInfo(
+ RegisteredServerResolver.resolve(server, plugin.connectionPlugin.connectionConfig.get().registration),
+ InetSocketAddress.createUnresolved(server.ip, server.port)
+ )
+ proxy.registerServer(info)
+ servers[server.serverId] = server
+ }
+
+ override fun unregister(server: RegisteredServer) {
+ val registered = proxy.getServer(
+ RegisteredServerResolver.resolve(server, plugin.connectionPlugin.connectionConfig.get().registration)
+ ).getOrNull() ?: return
+ proxy.unregisterServer(registered.serverInfo)
+ servers.remove(server.serverId)
+ }
+}
\ No newline at end of file
diff --git a/connection-waterdog/build.gradle.kts b/connection-waterdog/build.gradle.kts
new file mode 100644
index 0000000..e670452
--- /dev/null
+++ b/connection-waterdog/build.gradle.kts
@@ -0,0 +1,6 @@
+dependencies {
+ implementation(project(":connection-shared"))
+ implementation(libs.bundles.adventure)
+ compileOnly(libs.simplecloud.api)
+ compileOnly(libs.waterdog.api)
+}
\ No newline at end of file
diff --git a/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/WaterdogConnectionPlugin.kt b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/WaterdogConnectionPlugin.kt
new file mode 100644
index 0000000..058f59a
--- /dev/null
+++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/WaterdogConnectionPlugin.kt
@@ -0,0 +1,76 @@
+package app.simplecloud.plugin.connection.waterdog
+
+import app.simplecloud.api.CloudApi
+import app.simplecloud.plugin.connection.shared.ConnectionPlugin
+import app.simplecloud.plugin.connection.waterdog.command.ConnectionCommand
+import app.simplecloud.plugin.connection.waterdog.command.WaterdogCommandManager
+import app.simplecloud.plugin.connection.waterdog.handler.WaterdogJoinHandler
+import app.simplecloud.plugin.connection.waterdog.handler.WaterdogReconnectHandler
+import app.simplecloud.plugin.connection.waterdog.registration.WaterdogServerRegistry
+import dev.waterdog.waterdogpe.network.serverinfo.BedrockServerInfo
+import dev.waterdog.waterdogpe.plugin.Plugin
+import kotlinx.coroutines.*
+import org.apache.logging.log4j.LogManager
+import java.net.InetSocketAddress
+
+class WaterdogConnectionPlugin : Plugin() {
+
+ private val api = CloudApi.create()
+ private val logger = LogManager.getLogger(WaterdogConnectionPlugin::class.java)
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ private val commandManager = WaterdogCommandManager(this)
+
+ val connectionPlugin = ConnectionPlugin(
+ dataFolder.toString(),
+ api,
+ WaterdogServerRegistry(this, proxy)
+ )
+
+ override fun onEnable() {
+ cleanupServers()
+ registerAdditionalServers()
+ registerHandlers()
+ registerCommands()
+
+ scope.launch {
+ connectionPlugin.start()
+ }
+ }
+
+ override fun onDisable() {
+ commandManager.unregisterCommands()
+ scope.launch { connectionPlugin.shutdown() }
+ scope.cancel()
+ }
+
+ private fun cleanupServers() {
+ if (connectionPlugin.connectionConfig.get().registration.enabled) {
+ proxy.servers.forEach {
+ proxy.removeServerInfo(it.serverName)
+ }
+ }
+ }
+
+ private fun registerAdditionalServers() {
+ if (connectionPlugin.connectionConfig.get().registration.enabled) {
+ val additionalServers = connectionPlugin.connectionConfig.get().registration.additionalServers
+ additionalServers.forEach {
+ val address = InetSocketAddress.createUnresolved(it.address, it.port.toInt())
+ val info = BedrockServerInfo(it.name, address, address)
+ proxy.registerServerInfo(info)
+ logger.info("Additional server ${info.serverName} has been registered!")
+ }
+ }
+ }
+
+ private fun registerHandlers() {
+ proxy.joinHandler = WaterdogJoinHandler(this)
+ proxy.reconnectHandler = WaterdogReconnectHandler(this)
+ }
+
+ private fun registerCommands() {
+ commandManager.registerCommands()
+ proxy.commandMap.registerCommand(ConnectionCommand(this))
+ }
+
+}
diff --git a/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/ConnectionCommand.kt b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/ConnectionCommand.kt
new file mode 100644
index 0000000..03bfced
--- /dev/null
+++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/ConnectionCommand.kt
@@ -0,0 +1,57 @@
+package app.simplecloud.plugin.connection.waterdog.command
+
+import app.simplecloud.plugin.connection.waterdog.WaterdogConnectionPlugin
+import dev.waterdog.waterdogpe.command.Command
+import dev.waterdog.waterdogpe.command.CommandSender
+import dev.waterdog.waterdogpe.command.CommandSettings
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
+import org.apache.logging.log4j.LogManager
+
+class ConnectionCommand(
+ private val plugin: WaterdogConnectionPlugin,
+) : Command(
+ "connection",
+ CommandSettings.builder()
+ .setPermission("simplecloud.connection.reload")
+ .build()
+) {
+
+ private val logger = LogManager.getLogger(ConnectionCommand::class.java)
+ private val serializer = PlainTextComponentSerializer.plainText()
+
+ override fun onExecute(sender: CommandSender, alias: String?, args: Array): Boolean {
+ val messages = plugin.connectionPlugin.messageConfig.get()
+
+ if (args.firstOrNull()?.equals("reload", ignoreCase = true) != true) {
+ sendMessage(sender, messages.command.commandUsage)
+ return true
+ }
+
+ sendMessage(sender, messages.command.configReloading)
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ plugin.connectionPlugin.connectionConfig.reload()
+ plugin.connectionPlugin.commandConfig.reload()
+ plugin.connectionPlugin.messageConfig.reload()
+
+ sendMessage(sender, messages.command.configReloadedSuccess)
+ }
+ } catch (e: Exception) {
+ sendMessage(sender, messages.command.configReloadedFailed)
+ logger.error("Failed to reload config", e)
+ }
+
+ return true
+ }
+
+ private fun sendMessage(sender: CommandSender, rawMessage: String) {
+ val messages = plugin.connectionPlugin.messageConfig.get()
+ val component = messages.send(rawMessage)
+ val plain = serializer.serialize(component)
+ sender.sendMessage(plain)
+ }
+
+}
diff --git a/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/WaterdogCommandManager.kt b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/WaterdogCommandManager.kt
new file mode 100644
index 0000000..4228c2e
--- /dev/null
+++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/WaterdogCommandManager.kt
@@ -0,0 +1,111 @@
+package app.simplecloud.plugin.connection.waterdog.command
+
+import app.simplecloud.plugin.connection.shared.config.CommandEntry
+import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver
+import app.simplecloud.plugin.connection.waterdog.WaterdogConnectionPlugin
+import dev.waterdog.waterdogpe.command.Command
+import dev.waterdog.waterdogpe.command.CommandSender
+import dev.waterdog.waterdogpe.command.CommandSettings
+import dev.waterdog.waterdogpe.player.ProxiedPlayer
+import net.kyori.adventure.text.minimessage.MiniMessage
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
+
+class WaterdogCommandManager(
+ private val plugin: WaterdogConnectionPlugin,
+) {
+
+ private val commands = mutableListOf()
+ private val miniMessage = MiniMessage.miniMessage()
+ private val serializer = PlainTextComponentSerializer.plainText()
+
+ fun registerCommands() {
+ val commands = plugin.connectionPlugin.commandConfig.get().commands
+ for (command in commands) {
+ registerCommand(command)
+ }
+ }
+
+ fun unregisterCommands() {
+ commands.forEach { plugin.proxy.commandMap.unregisterCommand(it) }
+ commands.clear()
+ }
+
+ private fun registerCommand(command: CommandEntry) {
+ val settings = CommandSettings.builder()
+ .setAliases(*command.aliases.toTypedArray())
+ .apply {
+ if (command.permission.isNotEmpty()) {
+ permission = command.permission
+ }
+ }
+ .build()
+
+ val connectionCommand = object : Command(command.name, settings) {
+ override fun onExecute(sender: CommandSender, alias: String?, args: Array): Boolean {
+ val player = sender as? ProxiedPlayer ?: return false
+ handleCommand(player, command)
+ return true
+ }
+ }
+
+ plugin.proxy.commandMap.registerCommand(connectionCommand)
+ commands.add(command.name)
+ }
+
+ private fun handleCommand(player: ProxiedPlayer, command: CommandEntry) {
+ val config = plugin.connectionPlugin.connectionConfig.get()
+
+ if (command.permission.isNotEmpty() && !player.hasPermission(command.permission)) {
+ return
+ }
+
+ val currentServerName = player.serverInfo?.serverName
+ val serverNames = plugin.proxy.servers.map { it.serverName }
+ val sortedTargets = command.targetConnections.sortedByDescending { it.priority }
+
+ for (target in sortedTargets) {
+ if (target.from.isNotEmpty() && currentServerName != null) {
+ val isFromAllowed = target.from.any { connectionName ->
+ ConnectionResolver.isServerInConnection(
+ currentServerName, connectionName, config.connections, serverNames
+ )
+ }
+ if (!isFromAllowed) continue
+ }
+
+ val connection = ConnectionResolver.findConnection(target.name, config.connections) ?: continue
+
+ val failedRule = ConnectionResolver.checkRules(connection) { permission ->
+ player.hasPermission(permission)
+ }
+ if (failedRule != null) {
+ return
+ }
+
+ val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames)
+ if (matchingNames.isEmpty()) continue
+
+ val serverInfo = matchingNames
+ .mapNotNull { name -> plugin.proxy.getServerInfo(name) }
+ .minByOrNull { it.players.size }
+ ?: continue
+
+ if (currentServerName != null && serverInfo.serverName.equals(currentServerName, ignoreCase = true)) {
+ sendMessage(player, command.messages.alreadyConnected)
+ return
+ }
+
+ player.connect(serverInfo)
+ return
+ }
+
+ sendMessage(player, command.messages.noTargetConnectionFound)
+ }
+
+ private fun sendMessage(player: ProxiedPlayer, rawMessage: String) {
+ val component = miniMessage.deserialize(rawMessage)
+ val plain = serializer.serialize(component)
+ player.sendMessage(plain)
+ }
+
+}
diff --git a/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogJoinHandler.kt b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogJoinHandler.kt
new file mode 100644
index 0000000..206f78b
--- /dev/null
+++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogJoinHandler.kt
@@ -0,0 +1,42 @@
+package app.simplecloud.plugin.connection.waterdog.handler
+
+import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver
+import app.simplecloud.plugin.connection.waterdog.WaterdogConnectionPlugin
+import dev.waterdog.waterdogpe.network.connection.handler.IJoinHandler
+import dev.waterdog.waterdogpe.network.serverinfo.ServerInfo
+import dev.waterdog.waterdogpe.player.ProxiedPlayer
+
+class WaterdogJoinHandler(
+ private val plugin: WaterdogConnectionPlugin,
+) : IJoinHandler {
+
+ override fun determineServer(player: ProxiedPlayer): ServerInfo? {
+ val config = plugin.connectionPlugin.connectionConfig.get()
+
+ if (!config.networkJoinTargets.enabled) return null
+
+ val serverNames = plugin.proxy.servers.map { it.serverName }
+ val sortedTargets = config.networkJoinTargets.targetConnections.sortedByDescending { it.priority }
+
+ for (target in sortedTargets) {
+ val connection = ConnectionResolver.findConnection(target.name, config.connections) ?: continue
+
+ val failedRule = ConnectionResolver.checkRules(connection) { permission ->
+ player.hasPermission(permission)
+ }
+ if (failedRule != null) continue
+
+ val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames)
+ if (matchingNames.isEmpty()) continue
+
+ val serverInfo = matchingNames
+ .mapNotNull { name -> plugin.proxy.getServerInfo(name) }
+ .minByOrNull { it.players.size }
+
+ if (serverInfo != null) return serverInfo
+ }
+
+ return null
+ }
+
+}
diff --git a/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogReconnectHandler.kt b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogReconnectHandler.kt
new file mode 100644
index 0000000..7a2cb47
--- /dev/null
+++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogReconnectHandler.kt
@@ -0,0 +1,61 @@
+package app.simplecloud.plugin.connection.waterdog.handler
+
+import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver
+import app.simplecloud.plugin.connection.waterdog.WaterdogConnectionPlugin
+import dev.waterdog.waterdogpe.network.connection.handler.IReconnectHandler
+import dev.waterdog.waterdogpe.network.connection.handler.ReconnectReason
+import dev.waterdog.waterdogpe.network.serverinfo.ServerInfo
+import dev.waterdog.waterdogpe.player.ProxiedPlayer
+
+class WaterdogReconnectHandler(
+ private val plugin: WaterdogConnectionPlugin,
+) : IReconnectHandler {
+
+ override fun getFallbackServer(
+ player: ProxiedPlayer?,
+ oldServer: ServerInfo?,
+ reason: ReconnectReason?,
+ kickMessage: String?
+ ): ServerInfo? {
+ if (player == null) return null
+
+ val config = plugin.connectionPlugin.connectionConfig.get()
+
+ if (!config.fallback.enabled) return null
+
+ val kickedServerName = oldServer?.serverName
+ val serverNames = plugin.proxy.servers.map { it.serverName }
+ val sortedTargets = config.fallback.targetConnections.sortedByDescending { it.priority }
+
+ for (target in sortedTargets) {
+ if (target.from.isNotEmpty() && kickedServerName != null) {
+ val isFromAllowed = target.from.any { connectionName ->
+ ConnectionResolver.isServerInConnection(
+ kickedServerName, connectionName, config.connections, serverNames
+ )
+ }
+ if (!isFromAllowed) continue
+ }
+
+ val connection = ConnectionResolver.findConnection(target.name, config.connections) ?: continue
+
+ val failedRule = ConnectionResolver.checkRules(connection) { permission ->
+ player.hasPermission(permission)
+ }
+ if (failedRule != null) continue
+
+ val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames)
+ if (matchingNames.isEmpty()) continue
+
+ val serverInfo = matchingNames
+ .mapNotNull { name -> plugin.proxy.getServerInfo(name) }
+ .filter { it.serverName != kickedServerName }
+ .minByOrNull { it.players.size }
+
+ if (serverInfo != null) return serverInfo
+ }
+
+ return null
+ }
+
+}
diff --git a/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/registration/WaterdogServerRegistry.kt b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/registration/WaterdogServerRegistry.kt
new file mode 100644
index 0000000..08be8d3
--- /dev/null
+++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/registration/WaterdogServerRegistry.kt
@@ -0,0 +1,41 @@
+package app.simplecloud.plugin.connection.waterdog.registration
+
+import app.simplecloud.plugin.connection.shared.registration.RegisteredServer
+import app.simplecloud.plugin.connection.shared.registration.ServerRegistry
+import app.simplecloud.plugin.connection.shared.resolver.RegisteredServerResolver
+import app.simplecloud.plugin.connection.waterdog.WaterdogConnectionPlugin
+import dev.waterdog.waterdogpe.ProxyServer
+import dev.waterdog.waterdogpe.network.serverinfo.BedrockServerInfo
+import java.net.InetSocketAddress
+
+class WaterdogServerRegistry(
+ private val plugin: WaterdogConnectionPlugin,
+ private val proxy: ProxyServer
+) : ServerRegistry {
+
+ private val servers = mutableMapOf()
+
+ override fun getRegistered(): Map {
+ return servers
+ }
+
+ override fun register(server: RegisteredServer) {
+ val address = InetSocketAddress.createUnresolved(server.ip, server.port)
+ val info = BedrockServerInfo(
+ RegisteredServerResolver.resolve(server, plugin.connectionPlugin.connectionConfig.get().registration),
+ address,
+ address
+ )
+ proxy.registerServerInfo(info)
+ servers[server.serverId] = server
+ }
+
+ override fun unregister(server: RegisteredServer) {
+ val name = RegisteredServerResolver.resolve(
+ server,
+ plugin.connectionPlugin.connectionConfig.get().registration
+ )
+ proxy.removeServerInfo(name) ?: return
+ servers.remove(server.serverId)
+ }
+}
\ No newline at end of file
diff --git a/connection-waterdog/src/main/resources/plugin.yml b/connection-waterdog/src/main/resources/plugin.yml
new file mode 100644
index 0000000..ac5a7ef
--- /dev/null
+++ b/connection-waterdog/src/main/resources/plugin.yml
@@ -0,0 +1,5 @@
+name: simplecloud-connection
+version: 1.0.0
+author: InvalidJoker
+
+main: app.simplecloud.plugin.connection.waterdog.WaterDogConnectionPlugin
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f47f030..7abf4c9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,17 +1,53 @@
[versions]
-kotlin = "2.0.20"
-shadow = "8.3.3"
-sonatype-central-portal-publisher = "1.2.3"
-simplecloud-plugin = "0.0.1-dev.feb0927"
-minotaur = "2.8.7"
+kotlin = "2.2.20"
+kotlin-coroutines = "1.10.2"
+
+shadow = "9.3.2"
+minotaur = "2.8.10"
+
+simplecloud-api = "0.1.0-platform.20-dev.1771710355025-becd984"
+simplecloud-plugin-api = "0.0.1-platform.1766000824440-e57ed95"
+
+velocity = "3.5.0-SNAPSHOT"
+bungeecord = "1.21-R0.4"
+waterdog = "2.0.3"
+
+adventure-api = "4.26.1"
+adventure-platform-bungeecord = "4.4.1"
+
+configurate = "4.2.0"
+commons-io = "2.21.0"
+log4j = "2.25.3"
[libraries]
-kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
+kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
-simplecloud-plugin-api = { module = "app.simplecloud.plugin:plugin-shared", version.ref = "simplecloud-plugin" }
+kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
+
+simplecloud-api = { module = "app.simplecloud.api:api", version.ref = "simplecloud-api" }
+simplecloud-plugin-api = { module = "app.simplecloud.plugin:plugin-shared", version.ref = "simplecloud-plugin-api" }
+
+velocity-api = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" }
+bungeecord-api = { module = "net.md-5:bungeecord-api", version.ref = "bungeecord" }
+waterdog-api = { module = "dev.waterdog.waterdogpe:waterdog", version.ref = "waterdog" }
+
+adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure-api" }
+adventure-platform-bungeecord = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform-bungeecord" }
+adventure-text-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure-api" }
+adventure-text-serializer-plain = { module = "net.kyori:adventure-text-serializer-plain", version.ref = "adventure-api" }
+
+commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
+
+configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" }
+configurate-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" }
+
+log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
+
+[bundles]
+configurate = ["configurate-kotlin", "configurate-yaml"]
+adventure = ["adventure-api", "adventure-text-minimessage", "adventure-text-serializer-plain"]
[plugins]
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
-sonatype-central-portal-publisher = { id = "net.thebugmc.gradle.sonatype-central-portal-publisher", version.ref = "sonatype-central-portal-publisher" }
-minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" }
+minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0aaefbc..dbc3ce4 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 26bd757..e9ce7bb 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -8,9 +8,9 @@ pluginManagement {
}
plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
rootProject.name = "server-connection-plugin"
-include("connection-shared", "connection-velocity", "connection-bungeecord")
+include("connection-shared", "connection-velocity", "connection-bungeecord", "connection-waterdog")