diff --git a/.gitignore b/.gitignore index b63da45..5f3a971 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,11 @@ .gradle build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ +.idea/ -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ +### Kotlin ### +.kotlin/ ### VS Code ### .vscode/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 51024d5..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index dcc2d5f..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/LICENSE b/LICENSE index 94395cc..b085927 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [2024] [SimpleCloud] + Copyright [2024-2026] [SimpleCloud] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index ff85edb..9a6da76 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,19 @@ > All information about this project can be found in our detailed [documentation][docs-thisproject]. -The Server Connection Plugin provides comprehensive player connection management for your network, including network join handling, fallback servers, and navigation commands. +The Server Connection Plugin provides comprehensive player connection management for your network. It automatically registers SimpleCloud v3 servers on your proxy. ## Features - [x] **Velocity** - [x] **BungeeCord** +- [x] **Waterdog PE** - [ ] **Gate** -- [x] **Connection targets**: Connection targets define server groups that players can connect to -- [x] **Matcher Operations**: Match Server names by Operations +- [x] **Server registration**: Automatically registers SimpleCloud servers on your proxy +- [x] **Additional servers**: Add non network servers manually +- [x] **Connection targets**: Define server groups and persistent servers that players can connect to +- [x] **Fallback servers**: Automatically redirect players when a server becomes unavailable +- [x] **Matcher Operations**: Match server names by operations ## Contributing @@ -50,11 +54,11 @@ This repository is licensed under [Apache 2.0][license]. [banner]: https://github.com/simplecloudapp/branding/blob/main/readme/banner/plugin/server-connection.png?raw=true -[issue-bug-report]: https://github.com/theSimpleCloud/server-connection-plugin/issues/new?labels=bug&projects=template=01_BUG-REPORT.yml&title=%5BBUG%5D+%3Ctitle%3E +[issue-bug-report]: https://github.com/simplecloudapp/server-connection-plugin/issues/new?labels=bug&projects=template=01_BUG-REPORT.yml&title=%5BBUG%5D+%3Ctitle%3E -[issue-feature-request]: https://github.com/theSimpleCloud/server-connection-plugin/discussions/new?category=ideas +[issue-feature-request]: https://github.com/simplecloudapp/server-connection-plugin/discussions/new?category=ideas -[docs-thisproject]: https://docs.simplecloud.app/plugin/server-connection +[docs-thisproject]: https://docs.simplecloud.app/en/manual/plugin/server-connection [docs-contribute]: https://docs.simplecloud.app/contribute @@ -84,4 +88,4 @@ This repository is licensed under [Apache 2.0][license]. [badge-bluesky]: https://img.shields.io/badge/Follow_@simplecloud.app-d95652.svg?style=flat-square&logo=bluesky&color=27272a -[badge-youtube]: https://img.shields.io/badge/youtube-d95652.svg?style=flat-square&logo=youtube&color=27272a +[badge-youtube]: https://img.shields.io/badge/youtube-d95652.svg?style=flat-square&logo=youtube&color=27272a \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 398a12d..06b97b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,16 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { alias(libs.plugins.kotlin) alias(libs.plugins.shadow) - alias(libs.plugins.sonatype.central.portal.publisher) - `maven-publish` + id("maven-publish") } val baseVersion = "0.0.1" val commitHash = System.getenv("COMMIT_HASH") -val snapshotversion = "${baseVersion}-dev.$commitHash" +val snapshotversion = "${baseVersion}-platform.$commitHash" allprojects { group = "app.simplecloud.plugin" @@ -18,29 +19,33 @@ allprojects { repositories { mavenCentral() maven("https://buf.build/gen/maven") - maven("https://oss.sonatype.org/content/repositories/snapshots") - maven("https://libraries.minecraft.net") maven("https://repo.papermc.io/repository/maven-public") maven("https://repo.simplecloud.app/snapshots") + maven("https://repo.waterdog.dev/releases/") + maven("https://repo.waterdog.dev/snapshots/") + maven("https://repo.opencollab.dev/maven-releases/") + maven("https://repo.opencollab.dev/maven-snapshots/") } } subprojects { apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "com.gradleup.shadow") - apply(plugin = "net.thebugmc.gradle.sonatype-central-portal-publisher") apply(plugin = "maven-publish") dependencies { testImplementation(rootProject.libs.kotlin.test) - compileOnly(rootProject.libs.kotlin.jvm) + implementation(rootProject.libs.kotlin.jvm) + implementation(rootProject.libs.kotlin.coroutines.core) + implementation(rootProject.libs.log4j.api) } kotlin { jvmToolchain(21) compilerOptions { - apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + apiVersion.set(KotlinVersion.KOTLIN_2_0) + jvmTarget.set(JvmTarget.JVM_21) + freeCompilerArgs.add("-Xannotation-default-target=param-property") } } @@ -72,23 +77,12 @@ subprojects { } } - signing { - if (commitHash != null) { - return@signing - } - - sign(publishing.publications) - useGpgCmd() - } - tasks.named("shadowJar", ShadowJar::class) { mergeServiceFiles() + relocate("org.spongepowered", "app.simplecloud.plugin.relocate.spongepowered") + relocate("app.simplecloud.plugin.api", "app.simplecloud.plugin.relocate.plugin.api") archiveFileName.set("${project.name}.jar") - - val externalRelocatePath = "app.simplecloud.external" - relocate("kotlinx", "${externalRelocatePath}.kotlinx") - relocate("io", "${externalRelocatePath}.io") - relocate("org", "${externalRelocatePath}.org") + archiveClassifier.set("") } tasks.test { diff --git a/connection-bungeecord/build.gradle.kts b/connection-bungeecord/build.gradle.kts index d54bccb..46520d2 100644 --- a/connection-bungeecord/build.gradle.kts +++ b/connection-bungeecord/build.gradle.kts @@ -3,10 +3,11 @@ plugins { } dependencies { - api(project(":connection-shared")) - api("net.kyori:adventure-text-minimessage:4.16.0") - api("net.kyori:adventure-platform-bungeecord:4.3.2") - compileOnly("net.md-5:bungeecord-api:1.20-R0.2") + implementation(project(":connection-shared")) + implementation(libs.adventure.platform.bungeecord) + implementation(libs.bundles.adventure) + compileOnly(libs.simplecloud.api) + compileOnly(libs.bungeecord.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("bungeecord") loaders.add("waterfall") changelog.set("https://docs.simplecloud.app/changelog") diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordCommand.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordCommand.kt deleted file mode 100644 index 3e89b1e..0000000 --- a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordCommand.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.simplecloud.plugin.connection.bungeecord - -import app.simplecloud.plugin.connection.shared.ServerConnectionPlugin -import app.simplecloud.plugin.connection.shared.config.CommandConfig -import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer -import net.md_5.bungee.api.CommandSender -import net.md_5.bungee.api.ProxyServer -import net.md_5.bungee.api.connection.ProxiedPlayer -import net.md_5.bungee.api.plugin.Command - -/** - * @author Niklas Nieberler - */ - -class BungeeCordCommand( - private val serverConnection: ServerConnectionPlugin, - private val commandConfig: CommandConfig, - private val proxyServer: ProxyServer, - private val miniMessage: MiniMessage, -) : Command( - commandConfig.name, - commandConfig.permission, - *commandConfig.aliases.toTypedArray() -) { - - override fun execute(sender: CommandSender, args: Array) { - val player = sender as ProxiedPlayer? ?: return - - val currentServerName = player.server.info.name - val connectionToServerName = - this.serverConnection.getConnectionAndNameForCommand(player, this.commandConfig) - - if (connectionToServerName == null) { - player.sendMessage(*BungeeComponentSerializer.get().serialize(miniMessage.deserialize(commandConfig.noTargetConnectionFound))) - return - } - - if (currentServerName != null - && connectionToServerName.first.connectionConfig.serverNameMatcher.matches(currentServerName) - ) { - val miniMessageComponent = this.miniMessage.deserialize(this.commandConfig.alreadyConnectedMessage) - val component = BungeeComponentSerializer.get().serialize(miniMessageComponent) - player.sendMessage(*component) - return - } - - val serverInfo = this.proxyServer.getServerInfo(connectionToServerName.second) - if (serverInfo != null) { - player.connect(serverInfo) - } - } - -} \ No newline at end of file diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordConnectionPlugin.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordConnectionPlugin.kt new file mode 100644 index 0000000..13f9f39 --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordConnectionPlugin.kt @@ -0,0 +1,84 @@ +package app.simplecloud.plugin.connection.bungeecord + +import app.simplecloud.api.CloudApi +import app.simplecloud.plugin.connection.bungeecord.command.BungeeCordCommandManager +import app.simplecloud.plugin.connection.bungeecord.command.ConnectionCommand +import app.simplecloud.plugin.connection.bungeecord.listener.ServerConnectListener +import app.simplecloud.plugin.connection.bungeecord.listener.ServerKickListener +import app.simplecloud.plugin.connection.bungeecord.registration.BungeeCordServerRegistry +import app.simplecloud.plugin.connection.shared.ConnectionPlugin +import kotlinx.coroutines.* +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.md_5.bungee.api.plugin.Plugin +import org.apache.logging.log4j.LogManager +import java.net.InetSocketAddress + +class BungeeCordConnectionPlugin : Plugin() { + + private val api = CloudApi.create() + private val logger = LogManager.getLogger(BungeeCordConnectionPlugin::class.java) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val audiences = BungeeAudiences.create(this) + private val commandManager = BungeeCordCommandManager(this, audiences) + + val connectionPlugin = ConnectionPlugin( + dataFolder.toString(), + api, + BungeeCordServerRegistry(this, proxy) + ) + + override fun onEnable() { + cleanupServers() + registerAdditionalServers() + registerListeners() + registerCommands() + + scope.launch { + connectionPlugin.start() + } + } + + override fun onDisable() { + commandManager.unregisterCommands() + audiences.close() + scope.launch { connectionPlugin.shutdown() } + scope.cancel() + } + + private fun cleanupServers() { + if (connectionPlugin.connectionConfig.get().registration.enabled) { + proxy.servers.clear() + proxy.configurationAdapter.servers.clear() + proxy.configurationAdapter.listeners.forEach { + it.serverPriority.clear() + } + } + } + + private fun registerAdditionalServers() { + if (connectionPlugin.connectionConfig.get().registration.enabled) { + val additionalServers = connectionPlugin.connectionConfig.get().registration.additionalServers + additionalServers.forEach { + val info = proxy.constructServerInfo( + it.name, + InetSocketAddress.createUnresolved(it.address, it.port.toInt()), + it.name, + false + ) + proxy.servers[it.name] = info + logger.info("Additional server ${info.name} has been registered!") + } + } + } + + private fun registerListeners() { + proxy.pluginManager.registerListener(this, ServerConnectListener(this, audiences)) + proxy.pluginManager.registerListener(this, ServerKickListener(this, audiences)) + } + + private fun registerCommands() { + commandManager.registerCommands() + proxy.pluginManager.registerCommand(this, ConnectionCommand(this, audiences)) + } + +} diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordServerConnectionPlugin.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordServerConnectionPlugin.kt deleted file mode 100644 index feef894..0000000 --- a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/BungeeCordServerConnectionPlugin.kt +++ /dev/null @@ -1,79 +0,0 @@ -package app.simplecloud.plugin.connection.bungeecord - -import app.simplecloud.plugin.connection.shared.PermissionChecker -import app.simplecloud.plugin.connection.shared.ServerConnectionPlugin -import app.simplecloud.plugin.connection.shared.server.ServerConnectionInfo -import app.simplecloud.plugin.connection.shared.server.ServerConnectionInfoGetter -import net.kyori.adventure.text.minimessage.MiniMessage -import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer -import net.md_5.bungee.api.chat.TextComponent -import net.md_5.bungee.api.connection.ProxiedPlayer -import net.md_5.bungee.api.event.ServerKickEvent -import net.md_5.bungee.api.plugin.Listener -import net.md_5.bungee.api.plugin.Plugin -import net.md_5.bungee.event.EventHandler - -/** - * @author Niklas Nieberler - */ - -class BungeeCordServerConnectionPlugin : Plugin(), Listener { - - private val serverConnection = ServerConnectionPlugin( - dataFolder.toPath(), - ServerConnectionInfoGetter { - proxy.servers.map { - ServerConnectionInfo( - it.key, - it.value.players.size - ) - } - }, - PermissionChecker { player, permission -> player.hasPermission(permission) } - ) - - private val miniMessage = MiniMessage.miniMessage() - - override fun onLoad() { - proxy.reconnectHandler = ConnectionReconnectHandler(this.serverConnection, proxy) - } - - override fun onEnable() { - val pluginManager = proxy.pluginManager - pluginManager.registerListener(this, this) - - this.serverConnection.getCommandConfigs().forEach { - val bungeeCommand = BungeeCordCommand( - this.serverConnection, - it, - proxy, - miniMessage - ) - pluginManager.registerCommand(this, bungeeCommand) - } - } - - @EventHandler - fun onServerKick(event: ServerKickEvent) { - if (event.isCancelled) { - return - } - - val connectionAndTargetConfigToServerName = serverConnection.getConnectionAndNameForFallback(event.player, event.kickedFrom.name) - if (connectionAndTargetConfigToServerName == null) { - event.reason = TextComponent.fromArray(*BungeeComponentSerializer.get().serialize( - miniMessage.deserialize( - serverConnection.config.fallbackConnectionsConfig.noTargetConnectionFoundMessage - ) - )) - event.cancelServer = null - event.isCancelled = true - return - } - - val serverInfo = proxy.getServerInfo(connectionAndTargetConfigToServerName.second) ?: return - - event.isCancelled = true - event.cancelServer = serverInfo - } -} \ No newline at end of file diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/ConnectionReconnectHandler.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/ConnectionReconnectHandler.kt deleted file mode 100644 index 3cb04b1..0000000 --- a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/ConnectionReconnectHandler.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.simplecloud.plugin.connection.bungeecord - -import app.simplecloud.plugin.connection.shared.ServerConnectionPlugin -import net.md_5.bungee.api.ProxyServer -import net.md_5.bungee.api.ReconnectHandler -import net.md_5.bungee.api.config.ServerInfo -import net.md_5.bungee.api.connection.ProxiedPlayer - -/** - * @author Niklas Nieberler - */ - -class ConnectionReconnectHandler( - private val serverConnection: ServerConnectionPlugin, - private val proxyServer: ProxyServer, -) : ReconnectHandler { - - override fun getServer(player: ProxiedPlayer?): ServerInfo { - if (player == null) - throw NullPointerException("failed to find player") - val serverName = this.serverConnection.getServerNameForLogin(player) - ?: throw NullPointerException("failed to find connected server") - return this.proxyServer.getServerInfo(serverName) - } - - override fun setServer(player: ProxiedPlayer?) {} - - override fun save() {} - - override fun close() {} -} \ No newline at end of file diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/BungeeCordCommandManager.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/BungeeCordCommandManager.kt new file mode 100644 index 0000000..7dd77bb --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/BungeeCordCommandManager.kt @@ -0,0 +1,106 @@ +package app.simplecloud.plugin.connection.bungeecord.command + +import app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin +import app.simplecloud.plugin.connection.shared.config.CommandEntry +import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.md_5.bungee.api.CommandSender +import net.md_5.bungee.api.connection.ProxiedPlayer +import net.md_5.bungee.api.plugin.Command + +class BungeeCordCommandManager( + private val plugin: BungeeCordConnectionPlugin, + private val audiences: BungeeAudiences, +) { + + private val commands = mutableListOf() + + fun registerCommands() { + val commands = plugin.connectionPlugin.commandConfig.get().commands + for (command in commands) { + registerCommand(command) + } + } + + fun unregisterCommands() { + commands.forEach { plugin.proxy.pluginManager.unregisterCommand(findCommand(it)) } + commands.clear() + } + + private fun findCommand(name: String): Command? { + return plugin.proxy.pluginManager.commands.firstOrNull { + it.value.name.equals(name, ignoreCase = true) + }?.value + } + + private fun registerCommand(command: CommandEntry) { + val permission = command.permission.ifEmpty { null } + + val connectionCommand = object : Command( + command.name, + permission, + *command.aliases.toTypedArray() + ) { + override fun execute(sender: CommandSender, args: Array) { + val player = sender as? ProxiedPlayer ?: return + handleCommand(player, command) + } + } + + plugin.proxy.pluginManager.registerCommand(plugin, connectionCommand) + commands.add(command.name) + } + + private fun handleCommand(player: ProxiedPlayer, command: CommandEntry) { + val config = plugin.connectionPlugin.connectionConfig.get() + val messages = plugin.connectionPlugin.messageConfig.get() + val audience = audiences.player(player) + + if (command.permission.isNotEmpty() && !player.hasPermission(command.permission)) { + return + } + + val currentServerName = player.server?.info?.name + val serverNames = plugin.proxy.servers.keys.toList() + 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 targetServer = matchingNames + .mapNotNull { plugin.proxy.servers[it] } + .minByOrNull { it.players.size } + ?: continue + + if (currentServerName != null && targetServer.name.equals(currentServerName, ignoreCase = true)) { + audience.sendMessage(messages.send(command.messages.alreadyConnected)) + return + } + + player.connect(targetServer) + return + } + + audience.sendMessage(messages.send(command.messages.noTargetConnectionFound)) + } + +} diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/ConnectionCommand.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/ConnectionCommand.kt new file mode 100644 index 0000000..d7649dd --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/ConnectionCommand.kt @@ -0,0 +1,43 @@ +package app.simplecloud.plugin.connection.bungeecord.command + +import app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.md_5.bungee.api.CommandSender +import net.md_5.bungee.api.plugin.Command +import org.apache.logging.log4j.LogManager + +class ConnectionCommand( + private val plugin: BungeeCordConnectionPlugin, + private val audiences: BungeeAudiences, +) : Command("connection", "simplecloud.connection.reload") { + + private val logger = LogManager.getLogger(ConnectionCommand::class.java) + + override fun execute(sender: CommandSender, args: Array) { + val messages = plugin.connectionPlugin.messageConfig.get() + val audience = audiences.sender(sender) + + if (args.firstOrNull()?.equals("reload", ignoreCase = true) != true) { + audience.sendMessage(messages.send(messages.command.commandUsage)) + return + } + + audience.sendMessage(messages.send(messages.command.configReloading)) + try { + CoroutineScope(Dispatchers.IO).launch { + plugin.connectionPlugin.connectionConfig.reload() + plugin.connectionPlugin.commandConfig.reload() + plugin.connectionPlugin.messageConfig.reload() + + audience.sendMessage(messages.send(messages.command.configReloadedSuccess)) + } + } catch (e: Exception) { + audience.sendMessage(messages.send(messages.command.configReloadedFailed)) + logger.error("Failed to reload config", e) + } + } + +} diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerConnectListener.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerConnectListener.kt new file mode 100644 index 0000000..7fe10ff --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerConnectListener.kt @@ -0,0 +1,53 @@ +package app.simplecloud.plugin.connection.bungeecord.listener + +import app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin +import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.md_5.bungee.api.event.ServerConnectEvent +import net.md_5.bungee.api.plugin.Listener +import net.md_5.bungee.event.EventHandler + +class ServerConnectListener( + private val plugin: BungeeCordConnectionPlugin, + private val audiences: BungeeAudiences, +) : Listener { + + @EventHandler + fun onServerConnect(event: ServerConnectEvent) { + if (event.reason != ServerConnectEvent.Reason.JOIN_PROXY) return + + val config = plugin.connectionPlugin.connectionConfig.get() + val messages = plugin.connectionPlugin.messageConfig.get() + + if (!config.networkJoinTargets.enabled) return + + val serverNames = plugin.proxy.servers.keys.toList() + 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 targetServer = matchingNames + .mapNotNull { plugin.proxy.servers[it] } + .minByOrNull { it.players.size } + ?: continue + + event.target = targetServer + return + } + + event.isCancelled = true + val audience = audiences.player(event.player) + audience.sendMessage(messages.send(messages.kick.noTargetConnection)) + event.player.disconnect() + } + +} diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerKickListener.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerKickListener.kt new file mode 100644 index 0000000..4fe2ccf --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerKickListener.kt @@ -0,0 +1,63 @@ +package app.simplecloud.plugin.connection.bungeecord.listener + +import app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin +import app.simplecloud.plugin.connection.shared.connection.ConnectionResolver +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.md_5.bungee.api.event.ServerKickEvent +import net.md_5.bungee.api.plugin.Listener +import net.md_5.bungee.event.EventHandler + +class ServerKickListener( + private val plugin: BungeeCordConnectionPlugin, + private val audiences: BungeeAudiences, +) : Listener { + + @EventHandler + fun onServerKick(event: ServerKickEvent) { + val config = plugin.connectionPlugin.connectionConfig.get() + val messageConfig = plugin.connectionPlugin.messageConfig.get() + + if (!config.fallback.enabled) return + + val kickedServerName = event.kickedFrom.name + val serverNames = plugin.proxy.servers.keys.toList() + 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 targetServer = matchingNames + .mapNotNull { plugin.proxy.servers[it] } + .filter { it.name != kickedServerName } + .minByOrNull { it.players.size } + ?: continue + + event.cancelServer = targetServer + event.isCancelled = true + return + } + + event.isCancelled = true + val audience = audiences.player(event.player) + audience.sendMessage(messageConfig.send(messageConfig.kick.noFallbackServers)) + event.player.disconnect() + } + +} diff --git a/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/registration/BungeeCordServerRegistry.kt b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/registration/BungeeCordServerRegistry.kt new file mode 100644 index 0000000..be964c5 --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/registration/BungeeCordServerRegistry.kt @@ -0,0 +1,46 @@ +package app.simplecloud.plugin.connection.bungeecord.registration + +import app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin +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 net.md_5.bungee.api.ProxyServer +import net.md_5.bungee.api.config.ServerInfo +import java.net.InetSocketAddress + +class BungeeCordServerRegistry( + private val plugin: BungeeCordConnectionPlugin, + 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 name = RegisteredServerResolver.resolve( + server, + plugin.connectionPlugin.connectionConfig.get().registration + ) + val info: ServerInfo = ProxyServer.getInstance().constructServerInfo( + name, + address, + server.serverId, + server.properties.getOrDefault("proxy-restricted", "false").toString().toBoolean() + ) + proxy.servers[name] = info + servers[server.serverId] = server + } + + override fun unregister(server: RegisteredServer) { + val name = RegisteredServerResolver.resolve( + server, + plugin.connectionPlugin.connectionConfig.get().registration + ) + proxy.servers.remove(name) + servers.remove(server.serverId) + } +} \ No newline at end of file diff --git a/connection-bungeecord/src/main/resources/bungee.yml b/connection-bungeecord/src/main/resources/bungee.yml index 4e938e7..534d484 100644 --- a/connection-bungeecord/src/main/resources/bungee.yml +++ b/connection-bungeecord/src/main/resources/bungee.yml @@ -1,7 +1,5 @@ name: simplecloud-connection -version: 0.0.1 +version: 1.0.0 author: MrManHD -main: app.simplecloud.plugin.connection.bungeecord.BungeeCordServerConnectionPlugin - -depends: [simplecloud-api] \ No newline at end of file +main: app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin \ No newline at end of file diff --git a/connection-shared/build.gradle.kts b/connection-shared/build.gradle.kts index 43ee7a2..2bf1ad7 100644 --- a/connection-shared/build.gradle.kts +++ b/connection-shared/build.gradle.kts @@ -1,20 +1,7 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import org.gradle.kotlin.dsl.named - dependencies { - compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") - api("org.spongepowered:configurate-yaml:4.0.0") - api("org.spongepowered:configurate-extra-kotlin:4.1.2") { - exclude(group = "org.jetbrains.kotlin") - exclude(group = "org.jetbrains.kotlinx") - } - api("commons-io:commons-io:2.15.1") - api(rootProject.libs.simplecloud.plugin.api) -} - -tasks.named("shadowJar", ShadowJar::class) { - val externalRelocatePath = "app.simplecloud.external" - relocate("app.simplecloud.plugin.api", "${externalRelocatePath}.plugin.api") { - include("app.simplecloud.plugin.api.**") - } + compileOnly(libs.simplecloud.api) + api(libs.simplecloud.plugin.api) + implementation(libs.commons.io) + implementation(libs.bundles.adventure) + implementation(libs.bundles.configurate) } \ No newline at end of file diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionAndTargetConfig.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionAndTargetConfig.kt deleted file mode 100644 index 3334686..0000000 --- a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionAndTargetConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.simplecloud.plugin.connection.shared - -import app.simplecloud.plugin.connection.shared.config.ConnectionConfig -import app.simplecloud.plugin.connection.shared.config.TargetConnectionConfig - -data class ConnectionAndTargetConfig( - val connectionConfig: ConnectionConfig, - val targetConfig: TargetConnectionConfig, -) diff --git a/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionPlugin.kt b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionPlugin.kt new file mode 100644 index 0000000..785bc09 --- /dev/null +++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionPlugin.kt @@ -0,0 +1,64 @@ +package app.simplecloud.plugin.connection.shared + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.group.GroupServerType +import app.simplecloud.api.server.ServerQuery +import app.simplecloud.api.server.ServerState +import app.simplecloud.plugin.connection.shared.config.CommandConfig +import app.simplecloud.plugin.connection.shared.config.ConnectionConfig +import app.simplecloud.plugin.connection.shared.config.MessageConfig +import app.simplecloud.plugin.connection.shared.config.YamlConfig +import app.simplecloud.plugin.connection.shared.listener.ServerEventListener +import app.simplecloud.plugin.connection.shared.registration.ServerRegistry +import kotlinx.coroutines.future.await +import org.apache.logging.log4j.LogManager + +class ConnectionPlugin( + dir: String, + private val api: CloudApi, + registry: ServerRegistry, +) { + + private val logger = LogManager.getLogger(ConnectionPlugin::class.java) + private val listener = ServerEventListener(api, registry) + + val config = YamlConfig(dir) + + val connectionConfig = config.load("config") + val messageConfig = config.load("messages") + val commandConfig = config.load("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")