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..6f06ab2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,15 @@ +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` } 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 +18,32 @@ 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") } } @@ -50,45 +53,12 @@ subprojects { } } - publishing { - repositories { - maven { - name = "simplecloud" - url = uri("https://repo.simplecloud.app/snapshots/") - credentials { - username = System.getenv("SIMPLECLOUD_USERNAME")?: (project.findProperty("simplecloudUsername") as? String) - password = System.getenv("SIMPLECLOUD_PASSWORD")?: (project.findProperty("simplecloudPassword") as? String) - } - authentication { - create("basic") - } - } - } - - publications { - create("mavenJava") { - from(components["java"]) - } - } - } - - 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..4c8c9eb --- /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() + 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), + 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..f6cc25e --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/BungeeCordCommandManager.kt @@ -0,0 +1,107 @@ +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 +import java.util.concurrent.CopyOnWriteArrayList + +class BungeeCordCommandManager( + private val plugin: BungeeCordConnectionPlugin, + private val audiences: BungeeAudiences, +) { + + private val commands = CopyOnWriteArrayList() + + 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..caf5f96 --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/command/ConnectionCommand.kt @@ -0,0 +1,38 @@ +package app.simplecloud.plugin.connection.bungeecord.command + +import app.simplecloud.plugin.connection.bungeecord.BungeeCordConnectionPlugin +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)) + plugin.connectionPlugin.scope.launch { + try { + plugin.connectionPlugin.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..f81acf5 --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/listener/ServerConnectListener.kt @@ -0,0 +1,72 @@ +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() + + val virtualHost = event.player.pendingConnection.virtualHost?.hostName + if (virtualHost != null) { + val route = config.address.routes.find { it.subdomain == virtualHost } + if (route != null) { + val connection = ConnectionResolver.findConnection(route.targetConnection, config.connections) + if (connection != null) { + val serverNames = plugin.proxy.servers.keys.toList() + val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames) + val targetServer = matchingNames + .mapNotNull { plugin.proxy.servers[it] } + .minByOrNull { it.players.size } + if (targetServer != null) { + event.target = targetServer + return + } + } + } + } + + 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..359aa9b --- /dev/null +++ b/connection-bungeecord/src/main/kotlin/app/simplecloud/plugin/connection/bungeecord/registration/BungeeCordServerRegistry.kt @@ -0,0 +1,47 @@ +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 +import java.util.concurrent.ConcurrentHashMap + +class BungeeCordServerRegistry( + private val plugin: BungeeCordConnectionPlugin, + private val proxy: ProxyServer +) : ServerRegistry { + + private val servers = ConcurrentHashMap() + + 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..db037f7 100644 --- a/connection-shared/build.gradle.kts +++ b/connection-shared/build.gradle.kts @@ -1,20 +1,6 @@ -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.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..4349cb4 --- /dev/null +++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/ConnectionPlugin.kt @@ -0,0 +1,79 @@ +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.api.shared.config.ConfigurationFactory +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.listener.ServerEventListener +import app.simplecloud.plugin.connection.shared.registration.ServerRegistry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.future.await +import org.apache.logging.log4j.LogManager +import java.io.File + +class ConnectionPlugin( + dir: String, + private val api: CloudApi, + registry: ServerRegistry, +) { + + private val logger = LogManager.getLogger(ConnectionPlugin::class.java) + private val listener = ServerEventListener(api, registry) { connectionConfig.get().registration.ignoreServerGroupsAndPersistentServers } + + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + val connectionConfig = ConfigurationFactory(File(dir, "config.yml"), ConnectionConfig::class.java) + val messageConfig = ConfigurationFactory(File(dir, "messages.yml"), MessageConfig::class.java) + val commandConfig = ConfigurationFactory(File(dir, "commands.yml"), CommandConfig::class.java) + + init { + File(dir).mkdirs() + connectionConfig.loadOrCreate(ConnectionConfig()) + messageConfig.loadOrCreate(MessageConfig()) + commandConfig.loadOrCreate(CommandConfig()) + } + + suspend fun start() { + logger.info("SimpleCloud v3 connection plugin initialized!") + startRegistration() + } + + fun shutdown() { + logger.info("SimpleCloud v3 connection plugin uninitialized!") + if (connectionConfig.get().registration.enabled) { + listener.stop() + } + scope.cancel() + } + + fun reload() { + connectionConfig.loadOrCreate(ConnectionConfig()) + messageConfig.loadOrCreate(MessageConfig()) + commandConfig.loadOrCreate(CommandConfig()) + } + + 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..f94c370 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,28 @@ 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 +import org.spongepowered.configurate.objectmapping.meta.Comment @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(), + @Comment("Leave empty to allow everyone") val permission: String = "", + val messages: CommandMessages = CommandMessages(), + val targetConnections: List = listOf(), +) + +@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..7e514e0 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,106 @@ 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 +import org.spongepowered.configurate.objectmapping.meta.Comment @ConfigSerializable data class ConnectionConfig( + val version: Char = ConfigVersion.VERSION, + @Comment("Server Registration\nKeeps your proxy's registered child servers in sync with SimpleCloud.") + val registration: RegistrationConfig = RegistrationConfig(), + @Comment("Subdomain Routing\nRoutes players to a connection based on the hostname they connected with.") + val address: AddressConfig = AddressConfig(), + @Comment("Connections\nNamed groups that reference a set of servers by a name matcher.\nThese are referenced by network-join-targets, fallback and commands.yml.") + val connections: List = DefaultConfigs.CONNECTIONS, + @Comment("Network Join Targets\nDefines where players are sent when they first join the network.") + val networkJoinTargets: NetworkJoinTargetsConfig = DefaultConfigs.NETWORK_JOIN_TARGETS, + @Comment("Fallback\nDefines where players are sent when their current server becomes unavailable.") + val fallback: FallbackConfig = DefaultConfigs.FALLBACK, +) + +@ConfigSerializable +data class RegistrationConfig( + @Comment("If false, no SimpleCloud servers will be registered on the proxy.") + val enabled: Boolean = true, + @Comment("Pattern used to name dynamically started servers on the proxy.\nAvailable placeholders: , , , ") + val serverNamePattern: String = "-", + @Comment("Pattern used for persistent servers.") + val persistentServerNamePattern: String = "", + @Comment("Server groups and persistent servers that should not be registered on the proxy.") + val ignoreServerGroupsAndPersistentServers: List = listOf(), + @Comment("Non SimpleCloud servers to register manually.") + val additionalServers: List = listOf() +) + +@ConfigSerializable +data class RegistrationServer( + val name: String = "", + val address: String = "", + val port: Int = 0 +) + +@ConfigSerializable +data class AddressConfig( + val routes: List = listOf() +) + +@ConfigSerializable +data class SubdomainRoute( + val subdomain: String = "", + val targetConnection: String = "" +) + +@ConfigSerializable +data class ConnectionEntry( val name: String = "", + @Comment("Operation to match server names.\nAvailable: STARTS_WITH, ENDS_WITH, CONTAINS, EQUALS, REGEX, PATTERN, GREATER_THAN") val serverNameMatcher: ServerMatcherConfiguration = ServerMatcherConfiguration(), - val rules: List = emptyList() + @Comment("Rules that must pass before a player can use this connection.\nSee \"Connection Rules\" section below for details.") + val rules: List = listOf(), +) + +@ConfigSerializable +data class ConnectionRule( + val type: RuleType = RuleType.PERMISSION, + val name: String = "", + val value: String = "", + val operation: OperationType = OperationType.EQUALS, + @Comment("If true, inverts the match result.") + 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, + @Comment("Optional: only use this fallback if the player is coming from one of these servers.") + 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..2429f70 --- /dev/null +++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/config/MessageConfig.kt @@ -0,0 +1,45 @@ +package app.simplecloud.plugin.connection.shared.config + +import app.simplecloud.plugin.api.shared.extension.miniMessage +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.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import org.spongepowered.configurate.objectmapping.meta.Comment + +@ConfigSerializable +data class MessageConfig( + val version: Char = ConfigVersion.VERSION, + @Comment("Variables\nReusable variables that can be used throughout the messages\nUsage: will be replaced with the defined value") + val variables: Map = DefaultConfigs.VARIABLES, + @Comment("Kick Messages") + val kick: KickMessages = KickMessages(), + @Comment("Command Messages") + val command: ConnectionCommandMessages = ConnectionCommandMessages() +) { + 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/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..b1e6eec --- /dev/null +++ b/connection-shared/src/main/kotlin/app/simplecloud/plugin/connection/shared/listener/ServerEventListener.kt @@ -0,0 +1,96 @@ +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 ignoreList: () -> List, +) { + + 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 (ignoreList().any { it.equals(server.serverBaseName, ignoreCase = true) }) { + logger.info("Ignoring server ${server.serverId} (${server.serverBaseName}) due to ignore list") + 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..5f36b68 --- /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 = '2' + +} \ 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..2c7fec8 --- /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() + 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)) + 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..4a0b4f8 --- /dev/null +++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/ConnectionCommand.kt @@ -0,0 +1,41 @@ +package app.simplecloud.plugin.connection.velocity.command + +import app.simplecloud.plugin.connection.velocity.VelocityConnectionPlugin +import com.velocitypowered.api.command.SimpleCommand +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)) + plugin.connectionPlugin.scope.launch { + try { + plugin.connectionPlugin.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..9c18b66 --- /dev/null +++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/command/VelocityCommandManager.kt @@ -0,0 +1,107 @@ +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 +import java.util.concurrent.CopyOnWriteArrayList + +class VelocityCommandManager( + private val plugin: VelocityConnectionPlugin, + private val proxy: ProxyServer, +) { + private val commands = CopyOnWriteArrayList() + + 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..3133aea --- /dev/null +++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/listener/PlayerChooseInitialServerListener.kt @@ -0,0 +1,67 @@ +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() + + val virtualHost = event.player.virtualHost.map { it.hostName }.orElse(null) + if (virtualHost != null) { + val route = config.address.routes.find { it.subdomain == virtualHost } + if (route != null) { + val connection = ConnectionResolver.findConnection(route.targetConnection, config.connections) + if (connection != null) { + val serverNames = proxy.allServers.map { it.serverInfo.name } + val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames) + val server = matchingNames + .mapNotNull { proxy.getServer(it).orElse(null) } + .minByOrNull { it.playersConnected.size } + if (server != null) { + event.setInitialServer(server) + return + } + } + } + } + + 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..f759a20 --- /dev/null +++ b/connection-velocity/src/main/kotlin/app/simplecloud/plugin/connection/velocity/registration/VelocityServerRegistry.kt @@ -0,0 +1,40 @@ +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 java.util.concurrent.ConcurrentHashMap +import kotlin.jvm.optionals.getOrNull + +class VelocityServerRegistry( + private val plugin: VelocityConnectionPlugin, + private val proxy: ProxyServer +) : ServerRegistry { + + private val servers = ConcurrentHashMap() + + 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..2d36983 --- /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() + 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) + 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..3217f20 --- /dev/null +++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/ConnectionCommand.kt @@ -0,0 +1,52 @@ +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.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) + plugin.connectionPlugin.scope.launch { + try { + plugin.connectionPlugin.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..386982b --- /dev/null +++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/command/WaterdogCommandManager.kt @@ -0,0 +1,112 @@ +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 +import java.util.concurrent.CopyOnWriteArrayList + +class WaterdogCommandManager( + private val plugin: WaterdogConnectionPlugin, +) { + + private val commands = CopyOnWriteArrayList() + 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..fb20953 --- /dev/null +++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/handler/WaterdogJoinHandler.kt @@ -0,0 +1,56 @@ +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() + + val virtualHost = player.loginData.joinHostname + val route = config.address.routes.find { it.subdomain == virtualHost } + if (route != null) { + val connection = ConnectionResolver.findConnection(route.targetConnection, config.connections) + if (connection != null) { + val serverNames = plugin.proxy.servers.map { it.serverName } + val matchingNames = ConnectionResolver.findMatchingServerNames(connection, serverNames) + val serverInfo = matchingNames + .mapNotNull { name -> plugin.proxy.getServerInfo(name) } + .minByOrNull { it.players.size } + if (serverInfo != null) return serverInfo + } + } + + 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..9de3f46 --- /dev/null +++ b/connection-waterdog/src/main/kotlin/app/simplecloud/plugin/connection/waterdog/registration/WaterdogServerRegistry.kt @@ -0,0 +1,42 @@ +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 +import java.util.concurrent.ConcurrentHashMap + +class WaterdogServerRegistry( + private val plugin: WaterdogConnectionPlugin, + private val proxy: ProxyServer +) : ServerRegistry { + + private val servers = ConcurrentHashMap() + + 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..50fe030 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,50 @@ [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.4.1" +minotaur = "2.9.0" + +simplecloud-api = "0.1.0-platform.23-dev.1775473227066-1c0c25a" +simplecloud-plugin-api = "0.0.1-platform.1775946101240-f45d1bc" + +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" +log4j = "2.25.4" [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" } + +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..c61a118 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.1-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")